diff --git a/README.md b/README.md index 3490d22b0..a73233ce0 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ The continued development and maintenance of GitHub1s is made possible by these - [Mr-B0b/TamperMonkeyScripts/vscode.js](https://github.com/Mr-B0b/TamperMonkeyScripts/blob/main/vscode.js) -### Maintainers! :blush: +## Maintainers! :blush: @@ -182,3 +182,7 @@ The continued development and maintenance of GitHub1s is made possible by these

Siddhant Khare

💻 🖋
+ +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/conwnet/github1s.svg)](https://starchart.cc/conwnet/github1s) diff --git a/extensions/github1s/package.json b/extensions/github1s/package.json index a592b1ef8..2a589bc57 100644 --- a/extensions/github1s/package.json +++ b/extensions/github1s/package.json @@ -19,6 +19,7 @@ "onCommand:github1s.switch-to-commit", "onCommand:github1s.diff-view-open-left-file", "onCommand:github1s.diff-view-open-right-file", + "onCommand:github1s.open-on-github", "onView:github1s" ], "browser": "./dist/extension", @@ -198,6 +199,11 @@ "light": "assets/icons/light/close-blame.svg" }, "enablement": "!isInDiffEditor && resourceScheme =~ /^github1s$/" + }, + { + "command": "github1s.open-on-github", + "title": "Open on GitHub", + "category": "GitHub1s" } ], "colors": [ diff --git a/extensions/github1s/src/commands/commit.ts b/extensions/github1s/src/commands/commit.ts index 8f3941b26..289ca6e27 100644 --- a/extensions/github1s/src/commands/commit.ts +++ b/extensions/github1s/src/commands/commit.ts @@ -10,11 +10,12 @@ import { CommitTreeItem, getCommitTreeItemDescription, } from '@/views/commit-list-view'; +import { commitTreeDataProvider } from '@/views'; import { RequestNotFoundError } from '@/helpers/fetch'; const checkCommitExists = async (commitSha: string) => { try { - return !!(await repository.getCommit(commitSha)); + return !!(await repository.getCommitManager().getItem(commitSha)); } catch (e) { vscode.window.showErrorMessage( e instanceof RequestNotFoundError @@ -37,7 +38,7 @@ export const commandSwitchToCommit = async (commitSha?: string) => { }; // use the commit list as the candidates const commitItems: vscode.QuickPickItem[] = ( - await repository.getCommits((await router.getState()).ref) + await repository.getCommitManager().getList((await router.getState()).ref) ).map((commit) => ({ commitSha: commit.sha, label: commit.commit.message, @@ -97,3 +98,13 @@ export const commandCommitViewItemOpenOnGitHub = async ( const commitSha = viewItem?.commit?.sha; commitSha && commandOpenCommitOnGitHub(commitSha); }; + +export const commandCommitViewRefreshCommitList = (forceUpdate = true) => { + return commitTreeDataProvider.updateTree(forceUpdate); +}; + +export const commandCommitViewLoadMoreCommits = async () => { + const { ref } = await router.getState(); + repository.getCommitManager().loadMore(ref); + return commandCommitViewRefreshCommitList(false); +}; diff --git a/extensions/github1s/src/commands/editor.ts b/extensions/github1s/src/commands/editor.ts index d4ac2dc50..de10e62f7 100644 --- a/extensions/github1s/src/commands/editor.ts +++ b/extensions/github1s/src/commands/editor.ts @@ -94,7 +94,9 @@ const getLatestFileUri = async (fileUri: vscode.Uri) => { // to router.getAuthority() in this case const fileAuthority = fileUri.authority || (await router.getAuthority()); const [owner, repo, ref] = fileAuthority.split('+').filter(Boolean); - const latestCommitSha = await repository.getFileCommitSha(fileUri.path, ref); + const latestCommitSha = await repository + .getCommitManager() + .getFileCommitSha(fileUri.path, ref); return fileUri.with({ authority: `${owner}+${repo}+${latestCommitSha}`, @@ -116,10 +118,9 @@ export const commandEditorViewOpenPrevRevision = async ( const [owner, repo, rightCommitSha] = rightFileUri.authority .split('+') .filter(Boolean); - const leftCommitSha = await repository.getFilePrevCommitSha( - rightFileUri.path, - rightCommitSha - ); + const leftCommitSha = await repository + .getCommitManager() + .getFilePrevCommitSha(rightFileUri.path, rightCommitSha); // if we can't find prevCommitSha, use the the `emptyFileUri` as the leftFileUri const leftFileUri = leftCommitSha @@ -130,10 +131,9 @@ export const commandEditorViewOpenPrevRevision = async ( ? FileChangeType.MODIFIED : FileChangeType.ADDED; - const hasNextRevision = !!(await repository.getFileNextCommitSha( - rightFileUri.path, - rightCommitSha - )); + const hasNextRevision = !!(await repository + .getCommitManager() + .getFileNextCommitSha(rightFileUri.path, rightCommitSha)); const query = queryString.stringify({ base: leftFileUri.with({ query: '' }).toString(), @@ -166,10 +166,9 @@ export const commandEditorViewOpenNextRevision = async ( const [owner, repo, leftCommitSha] = leftFileUri.authority .split('+') .filter(Boolean); - const rightCommitSha = await repository.getFileNextCommitSha( - leftFileUri.path, - leftCommitSha - ); + const rightCommitSha = await repository + .getCommitManager() + .getFileNextCommitSha(leftFileUri.path, leftCommitSha); if (!rightCommitSha) { return vscode.window.showInformationMessage( @@ -181,10 +180,9 @@ export const commandEditorViewOpenNextRevision = async ( authority: `${owner}+${repo}+${rightCommitSha}`, }); - const hasNextRevision = !!(await repository.getFileNextCommitSha( - rightFileUri.path, - rightCommitSha - )); + const hasNextRevision = !!(await repository + .getCommitManager() + .getFileNextCommitSha(rightFileUri.path, rightCommitSha)); const query = queryString.stringify({ base: leftFileUri.with({ query: '' }).toString(), head: rightFileUri.with({ query: '' }).toString(), diff --git a/extensions/github1s/src/commands/global.ts b/extensions/github1s/src/commands/global.ts new file mode 100644 index 000000000..9b12ed8a4 --- /dev/null +++ b/extensions/github1s/src/commands/global.ts @@ -0,0 +1,19 @@ +/** + * @file GitHub1s Ref Related Commands + * @author netcon + */ + +import * as vscode from 'vscode'; +import router from '@/router'; + +export const commandOpenOnGitHub = async () => { + const location = router.history.location; + const githubPath = + location.pathname === '/' + ? '/conwnet/github1s' + : `${location.pathname}${location.search}${location.hash}`; + const GITHUB_ORIGIN = 'https://github.com'; + const gitHubUri = vscode.Uri.parse(GITHUB_ORIGIN + githubPath); + + return vscode.commands.executeCommand('vscode.open', gitHubUri); +}; diff --git a/extensions/github1s/src/commands/index.ts b/extensions/github1s/src/commands/index.ts index 101a3ef62..871e6c8f2 100644 --- a/extensions/github1s/src/commands/index.ts +++ b/extensions/github1s/src/commands/index.ts @@ -5,7 +5,6 @@ import * as vscode from 'vscode'; import { getExtensionContext } from '@/helpers/context'; -import { pullRequestTreeDataProvider, commitTreeDataProvider } from '@/views'; import { commandValidateToken, commandUpdateToken, @@ -16,12 +15,16 @@ import { commandSwitchToPull, commandPullViewItemSwitchToPull, commandPullViewItemOpenOnGitHub, + commandPullViewRefreshPullList, + commandPullViewLoadMorePulls, } from './pull'; import { commandSwitchToCommit, commandOpenCommitOnGitHub, commandCommitViewItemSwitchToCommit, commandCommitViewItemOpenOnGitHub, + commandCommitViewRefreshCommitList, + commandCommitViewLoadMoreCommits, } from './commit'; import { commandOpenGitpod } from './gitpod'; import { @@ -36,6 +39,7 @@ import { commandOpenEditorGutterBlame, commandCloseEditorGutterBlame, } from './blame'; +import { commandOpenOnGitHub } from './global'; const commands: { id: string; callback: (...args: any[]) => any }[] = [ // validate GitHub OAuth Token @@ -53,7 +57,9 @@ const commands: { id: string; callback: (...args: any[]) => any }[] = [ // switch to a pull request & input pull number manually { id: 'github1s.switch-to-pull', callback: commandSwitchToPull }, // update the pull request list in the pull requests view - { id: 'github1s.pull-view-refresh-pull-list', callback: () => pullRequestTreeDataProvider.updateTree() }, // prettier-ignore + { id: 'github1s.pull-view-refresh-pull-list', callback: commandPullViewRefreshPullList }, // prettier-ignore + // load more pulls in the pull requests tree view + { id: 'github1s.pull-view-load-more-pulls', callback: commandPullViewLoadMorePulls }, // prettier-ignore // switch to a pull request in the pull requests view { id: 'github1s.pull-view-item-switch-to-pull', callback: commandPullViewItemSwitchToPull }, // prettier-ignore // open pull on github in the pull requests view @@ -64,7 +70,9 @@ const commands: { id: string; callback: (...args: any[]) => any }[] = [ // open a commit on GitHub's website { id: 'github1s.open-commit-on-github', callback: commandOpenCommitOnGitHub }, // update the commit list in the commits view - { id: 'github1s.commit-view-refresh-commit-list', callback: () => commitTreeDataProvider.updateTree() }, // prettier-ignore + { id: 'github1s.commit-view-refresh-commit-list', callback: commandCommitViewRefreshCommitList }, // prettier-ignore + // load more commits in the commits tree view + { id: 'github1s.commit-view-load-more-commits', callback: commandCommitViewLoadMoreCommits }, // prettier-ignore // switch to a commit in the commits view { id: 'github1s.commit-view-item-switch-to-commit', callback: commandCommitViewItemSwitchToCommit }, // prettier-ignore // open commit on github in the commits view @@ -90,6 +98,9 @@ const commands: { id: string; callback: (...args: any[]) => any }[] = [ { id: 'github1s.open-editor-gutter-blame', callback: commandOpenEditorGutterBlame }, // prettier-ignore // close the gutter blame of a editor { id: 'github1s.close-editor-gutter-blame', callback: commandCloseEditorGutterBlame }, // prettier-ignore + + // open current page on GitHub + { id: 'github1s.open-on-github', callback: commandOpenOnGitHub }, ]; export const registerGitHub1sCommands = () => { diff --git a/extensions/github1s/src/commands/pull.ts b/extensions/github1s/src/commands/pull.ts index d90da6db7..86edb412b 100644 --- a/extensions/github1s/src/commands/pull.ts +++ b/extensions/github1s/src/commands/pull.ts @@ -11,11 +11,12 @@ import { getPullTreeItemLabel, getPullTreeItemDescription, } from '@/views/pull-list-view'; +import { pullRequestTreeDataProvider } from '@/views'; import { RequestNotFoundError } from '@/helpers/fetch'; const checkPullExists = async (pullNumber: number) => { try { - return !!(await repository.getPull(pullNumber)); + return !!(await repository.getPullManager().getItem(pullNumber)); } catch (e) { vscode.window.showErrorMessage( e instanceof RequestNotFoundError @@ -38,7 +39,7 @@ export const commandSwitchToPull = async (pullNumber?: number) => { }; // use the pull list as the candidates const pullRequestItems: vscode.QuickPickItem[] = ( - await repository.getPulls() + await repository.getPullManager().getList() ).map((pull) => ({ pullNumber: pull.number, label: getPullTreeItemLabel(pull), @@ -98,3 +99,12 @@ export const commandPullViewItemOpenOnGitHub = async ( ); } }; + +export const commandPullViewRefreshPullList = (forceUpdate = true) => { + return pullRequestTreeDataProvider.updateTree(forceUpdate); +}; + +export const commandPullViewLoadMorePulls = () => { + repository.getPullManager().loadMore(); + return commandPullViewRefreshPullList(false); +}; diff --git a/extensions/github1s/src/interfaces/github-api-rest.ts b/extensions/github1s/src/interfaces/github-api-rest.ts index 2b1b8fd57..33ba719c5 100644 --- a/extensions/github1s/src/interfaces/github-api-rest.ts +++ b/extensions/github1s/src/interfaces/github-api-rest.ts @@ -138,11 +138,13 @@ export const getGitHubAllFiles = ( export const getGitHubPulls = ( owner: string, repo: string, + pageNumber = 0, + pageSize = 100, options?: RequestInit ) => { // TODO: only recent 100 pull requests are supported now return fetch( - `https://api.github.com/repos/${owner}/${repo}/pulls?state=all&order=created&per_page=100`, + `https://api.github.com/repos/${owner}/${repo}/pulls?state=all&order=created&per_page=${pageSize}&page=${pageNumber}`, options ); }; @@ -176,10 +178,12 @@ export const getGitHubCommits = ( owner: string, repo: string, sha: string, + pageNumber = 0, + pageSize = 100, options?: ResponseInit ) => { return fetch( - `https://api.github.com/repos/${owner}/${repo}/commits?sha=${sha}&per_page=100`, + `https://api.github.com/repos/${owner}/${repo}/commits?sha=${sha}&per_page=${pageSize}&page=${pageNumber}`, options ); }; diff --git a/extensions/github1s/src/providers/changedFileDecorationProvider.ts b/extensions/github1s/src/providers/changedFileDecorationProvider.ts index c53549d82..49fef98fd 100644 --- a/extensions/github1s/src/providers/changedFileDecorationProvider.ts +++ b/extensions/github1s/src/providers/changedFileDecorationProvider.ts @@ -73,7 +73,9 @@ const getFileDecorationForPull = async ( uri: Uri, pullNumber: number ): Promise => { - const changedFiles = await repository.getPullFiles(pullNumber); + const changedFiles = await repository + .getPullManager() + .getPullFiles(pullNumber); return getFileDecorationFromChangeFiles(uri, changedFiles); }; @@ -81,7 +83,9 @@ const getFileDecorationForCommit = async ( uri: Uri, commitSha: string ): Promise => { - const changedFiles = await repository.getCommitFiles(commitSha); + const changedFiles = await repository + .getCommitManager() + .getCommitFiles(commitSha); return getFileDecorationFromChangeFiles(uri, changedFiles); }; diff --git a/extensions/github1s/src/repository/github-commit-manager.ts b/extensions/github1s/src/repository/github-commit-manager.ts new file mode 100644 index 000000000..88e51aa15 --- /dev/null +++ b/extensions/github1s/src/repository/github-commit-manager.ts @@ -0,0 +1,182 @@ +/** + * @file GitHub pull manager + * @author netcon + */ + +import { + getGitHubCommits, + getGitHubCommitDetail, + getGitHubFileCommits, +} from '@/interfaces/github-api-rest'; +import { getFetchOptions } from '@/helpers/fetch'; +import { reuseable } from '@/helpers/func'; +import { Barrier } from '@/helpers/async'; +import { LinkedList, LinkedListDirection } from './linked-list'; +import { Repository } from './index'; +import { + CommitManager, + RepositoryCommit, + RepositoryChangedFile, +} from './types'; + +export class GitHubCommitManager implements CommitManager { + private _commitMap = new Map(); + private _fileCommitIdListMap = new Map(); + private _commitListMap = new Map>(); + private _pageSize = 100; + private _currentPageNumber = 1; // page number is begin from 1 + private _hasMore = true; + private _loadingBarrier = new Barrier(); + + constructor(public repository: Repository) { + this._loadingBarrier.open(); + } + + getList = reuseable( + async ( + commitSha: string, + forceUpdate: boolean = false + ): Promise => { + if (forceUpdate || !this._commitListMap.has(commitSha)) { + this._commitListMap.set(commitSha, []); + this._currentPageNumber = 0; + this.loadMore(commitSha); + } + await this._loadingBarrier.wait(); + return this._commitListMap.get(commitSha); + } + ); + + getItem = reuseable( + async ( + commitSha: string, + forceUpdate: boolean = false + ): Promise => { + if (forceUpdate || !this._commitMap.has(commitSha)) { + const commit = await getGitHubCommitDetail( + this.repository.getOwner(), + this.repository.getRepo(), + commitSha, + getFetchOptions(forceUpdate) + ); + this._commitMap.set(commitSha, commit); + } + return this._commitMap.get(commitSha); + } + ); + + async loadMore(commitSha: string) { + this._loadingBarrier = new Barrier(); + const fetchOptions = getFetchOptions(true); + const commits = await getGitHubCommits( + this.repository.getOwner(), + this.repository.getRepo(), + commitSha, + this._currentPageNumber, + this._pageSize, + fetchOptions + ); + + commits.forEach((commit) => this._commitMap.set(commit.sha, commit)); + this._commitListMap.get(commitSha).push(...commits); + this._currentPageNumber += 1; + this._hasMore = commits.length === this._pageSize; + this._loadingBarrier.open(); + + return this._hasMore; + } + + async hasMore() { + await this._loadingBarrier.wait(); + return this._hasMore; + } + + public getCommitFiles = reuseable( + async ( + commitSha: string, + forceUpdate: boolean = false + ): Promise => { + return ( + // the commit maybe updated by fetch commit **list** which + // won't have the file list data, so we will fallback to + // fetch single commit data to get the file list data + (await this.getItem(commitSha, forceUpdate)).files || + (await this.getItem(commitSha, true)).files + ); + } + ); + + // get this commits for a specified file + public getFileCommits = reuseable( + async ( + filePath: string, + commitSha: string, + forceUpdate: boolean = false + ): Promise => { + const commits = await getGitHubFileCommits( + this.repository.getOwner(), + this.repository.getRepo(), + filePath, + commitSha, + getFetchOptions(forceUpdate) + ); + // `this.getCommit` can be benefited from the cache + commits.forEach((commit) => this._commitMap.set(commit.sha, commit)); + return commits; + } + ); + + // get the commit sha of a file with direction, default get + // the latest commit sha for the file, note this the latest + // commit sha maybe not equal the `commitSha` in arguments + public getFileCommitSha = reuseable( + async ( + filePath: string, + commitShaOrRef: string, + direction: LinkedListDirection = LinkedListDirection.CURRENT, + forceUpdate: boolean = false + ): Promise => { + if (!this._fileCommitIdListMap.has(filePath)) { + this._fileCommitIdListMap.set(filePath, new LinkedList()); + } + const commitIdList = this._fileCommitIdListMap.get(filePath); + if (!commitIdList.getNodeId(commitShaOrRef, direction)) { + const commits = await this.getFileCommits( + filePath, + commitShaOrRef, + forceUpdate + ); + commitIdList.update(commits.map((item) => item.sha).reverse()); + // Actually the latest commit for `filePath` maybe not equal the + // `commitSha` in arguments, we should use commits[0].sha in this case + return commitIdList.getNodeId(commits[0]?.sha, direction); + } + return commitIdList.getNodeId(commitShaOrRef, direction); + } + ); + + public getFilePrevCommitSha = reuseable( + async ( + filePath: string, + commitSha: string, + forceUpdate: boolean = false + ): Promise => { + return this.getFileCommitSha( + filePath, + commitSha, + LinkedListDirection.PREVIOUS, + forceUpdate + ); + } + ); + + public getFileNextCommitSha = reuseable( + async (filePath: string, commitSha: string): Promise => { + // because we can not find the next commit by GitHub API, + // we can only try to find the next commit from the cache + return this._fileCommitIdListMap + .get(filePath) + ?.getNodeId(commitSha, LinkedListDirection.NEXT); + } + ); +} diff --git a/extensions/github1s/src/repository/github-pull-manager.ts b/extensions/github1s/src/repository/github-pull-manager.ts new file mode 100644 index 000000000..c396a33d8 --- /dev/null +++ b/extensions/github1s/src/repository/github-pull-manager.ts @@ -0,0 +1,97 @@ +/** + * @file GitHub pull manager + * @author netcon + */ + +import { + getGitHubPulls, + getGitHubPullFiles, + getGitHubPullDetail, +} from '@/interfaces/github-api-rest'; +import { getFetchOptions } from '@/helpers/fetch'; +import { reuseable } from '@/helpers/func'; +import { Barrier } from '@/helpers/async'; +import { Repository } from './index'; +import { PullManager, RepositoryPull, RepositoryChangedFile } from './types'; + +export class GitHubPullManager implements PullManager { + private _pullMap = new Map(); + private _pullList = null; + private _pageSize = 100; + private _currentPageNumber = 1; // page number is begin from 1 + private _hasMore = true; + private _loadingBarrier = new Barrier(); + + constructor(public repository: Repository) { + this._loadingBarrier.open(); + } + + getList = reuseable( + async (forceUpdate: boolean = false): Promise => { + if (forceUpdate || !this._pullList) { + this._pullList = []; + this._currentPageNumber = 0; + this.loadMore(); + } + await this._loadingBarrier.wait(); + return this._pullList; + } + ); + + getItem = reuseable( + async ( + pullNumber: number, + forceUpdate: boolean = false + ): Promise => { + if (forceUpdate || !this._pullMap.has(pullNumber)) { + const pull = await getGitHubPullDetail( + this.repository.getOwner(), + this.repository.getRepo(), + pullNumber, + getFetchOptions(forceUpdate) + ); + this._pullMap.set(pullNumber, pull); + } + return this._pullMap.get(pullNumber); + } + ); + + async loadMore() { + this._loadingBarrier = new Barrier(); + const fetchOptions = getFetchOptions(true); + const pulls = await getGitHubPulls( + this.repository.getOwner(), + this.repository.getRepo(), + this._currentPageNumber, + this._pageSize, + fetchOptions + ); + + pulls.forEach((pull) => this._pullMap.set(pull.number, pull)); + this._pullList.push(...pulls); + this._currentPageNumber += 1; + this._hasMore = pulls.length === this._pageSize; + this._loadingBarrier.open(); + + return this._hasMore; + } + + async hasMore() { + await this._loadingBarrier.wait(); + return this._hasMore; + } + + public getPullFiles = reuseable( + async ( + pullNumber: number, + forceUpdate: boolean = false + ): Promise => { + return getGitHubPullFiles( + this.repository.getOwner(), + this.repository.getRepo(), + pullNumber, + getFetchOptions(forceUpdate) + ); + } + ); +} diff --git a/extensions/github1s/src/repository/index.ts b/extensions/github1s/src/repository/index.ts index ed9099136..ae833e2b0 100644 --- a/extensions/github1s/src/repository/index.ts +++ b/extensions/github1s/src/repository/index.ts @@ -7,32 +7,20 @@ import { reuseable } from '@/helpers/func'; import router from '@/router'; import { getGitHubBranchRefs, - getGitHubPullDetail, getGitHubTagRefs, - getGitHubPullFiles, - getGitHubPulls, - getGitHubCommitDetail, - getGitHubCommits, - getGitHubFileCommits, } from '@/interfaces/github-api-rest'; import { apolloClient } from '@/interfaces/client'; import { githubFileBlameQuery } from '@/interfaces/github-api-gql'; import { getFetchOptions } from '@/helpers/fetch'; -import { LinkedList, LinkedListDirection } from './linked-list'; -import { - RepositoryChangedFile, - RepositoryCommit, - RepositoryPull, - RepositoryRef, - BlameRange, -} from './types'; +import { GitHubPullManager } from './github-pull-manager'; +import { GitHubCommitManager } from './github-commit-manager'; +import { RepositoryRef, BlameRange, PullManager } from './types'; export class Repository { private static instance: Repository; - private _fileCommitIdListMap: Map; - private _pullMap: Map; - private _commitMap: Map; private _fileBlameMap: Map; + private _pullManager: PullManager; + private _commitManager: GitHubCommitManager; public static getInstance() { if (Repository.instance) { @@ -42,12 +30,19 @@ export class Repository { } constructor() { - this._fileCommitIdListMap = new Map(); - this._pullMap = new Map(); - this._commitMap = new Map(); + this._pullManager = new GitHubPullManager(this); + this._commitManager = new GitHubCommitManager(this); this._fileBlameMap = new Map(); } + public getPullManager(): GitHubPullManager { + return this._pullManager as GitHubPullManager; + } + + public getCommitManager(): GitHubCommitManager { + return this._commitManager as GitHubCommitManager; + } + // get current repo owner public getOwner() { const pathname = router.history.location.pathname; @@ -64,11 +59,7 @@ export class Repository { public getBranches = reuseable( async (forceUpdate: boolean = false): Promise => { const [owner, repo] = [this.getOwner(), this.getRepo()]; - return await getGitHubBranchRefs( - owner, - repo, - getFetchOptions(forceUpdate) - ); + return getGitHubBranchRefs(owner, repo, getFetchOptions(forceUpdate)); } ); @@ -80,174 +71,6 @@ export class Repository { } ); - public getPulls = reuseable( - async (forceUpdate: boolean = false): Promise => { - const [owner, repo] = [this.getOwner(), this.getRepo()]; - const fetchOptions = getFetchOptions(forceUpdate); - const pulls = await getGitHubPulls(owner, repo, fetchOptions); - // `this.getPull` can be benefited from the cache - pulls.forEach((pull) => this._pullMap.set(pull.number, pull)); - return pulls; - } - ); - - public getPull = reuseable( - async ( - pullNumber: number, - forceUpdate: boolean = false - ): Promise => { - const [owner, repo] = [this.getOwner(), this.getRepo()]; - if (forceUpdate || !this._pullMap.has(pullNumber)) { - const pull = await getGitHubPullDetail( - owner, - repo, - pullNumber, - getFetchOptions(forceUpdate) - ); - this._pullMap.set(pullNumber, pull); - } - return this._pullMap.get(pullNumber); - } - ); - - public getPullFiles = reuseable( - async ( - pullNumber: number, - forceUpdate: boolean = false - ): Promise => { - const [owner, repo] = [this.getOwner(), this.getRepo()]; - return getGitHubPullFiles( - owner, - repo, - pullNumber, - getFetchOptions(forceUpdate) - ); - } - ); - - public getCommits = reuseable( - async ( - sha: string, - forceUpdate: boolean = false - ): Promise => { - const [owner, repo] = [this.getOwner(), this.getRepo()]; - const fetchOptions = getFetchOptions(forceUpdate); - const commits = await getGitHubCommits(owner, repo, sha, fetchOptions); - // `this.getCommit` can be benefited from the cache - commits.forEach((commit) => this._commitMap.set(commit.sha, commit)); - return commits; - } - ); - - public getCommit = reuseable( - async ( - commitSha: string, - forceUpdate: boolean = false - ): Promise => { - const [owner, repo] = [this.getOwner(), this.getRepo()]; - if (forceUpdate || !this._commitMap.has(commitSha)) { - const commit = await getGitHubCommitDetail( - owner, - repo, - commitSha, - getFetchOptions(forceUpdate) - ); - this._commitMap.set(commitSha, commit); - } - return this._commitMap.get(commitSha); - } - ); - - public getCommitFiles = reuseable( - async ( - commitSha: string, - forceUpdate: boolean = false - ): Promise => { - return ( - // the commit maybe updated by fetch commit **list** which - // won't have the file list data, so we will fallback to - // fetch single commit data to get the file list data - (await this.getCommit(commitSha, forceUpdate)).files || - (await this.getCommit(commitSha, true)).files - ); - } - ); - - // get this commits for a specified file - public getFileCommits = reuseable( - async ( - filePath: string, - commitSha: string, - forceUpdate: boolean = false - ): Promise => { - const [owner, repo] = [this.getOwner(), this.getRepo()]; - const commits = await getGitHubFileCommits( - owner, - repo, - filePath, - commitSha, - getFetchOptions(forceUpdate) - ); - // `this.getCommit` can be benefited from the cache - commits.forEach((commit) => this._commitMap.set(commit.sha, commit)); - return commits; - } - ); - - // get the commit sha of a file with direction, default get - // the latest commit sha for the file, note this the latest - // commit sha maybe not equal the `commitSha` in arguments - public getFileCommitSha = reuseable( - async ( - filePath: string, - commitShaOrRef: string, - direction: LinkedListDirection = LinkedListDirection.CURRENT, - forceUpdate: boolean = false - ): Promise => { - if (!this._fileCommitIdListMap.has(filePath)) { - this._fileCommitIdListMap.set(filePath, new LinkedList()); - } - const commitIdList = this._fileCommitIdListMap.get(filePath); - if (!commitIdList.getNodeId(commitShaOrRef, direction)) { - const commits = await this.getFileCommits( - filePath, - commitShaOrRef, - forceUpdate - ); - commitIdList.update(commits.map((item) => item.sha).reverse()); - // Actually the latest commit for `filePath` maybe not equal the - // `commitSha` in arguments, we should use commits[0].sha in this case - return commitIdList.getNodeId(commits[0]?.sha, direction); - } - return commitIdList.getNodeId(commitShaOrRef, direction); - } - ); - - public getFilePrevCommitSha = reuseable( - async ( - filePath: string, - commitSha: string, - forceUpdate: boolean = false - ): Promise => { - return this.getFileCommitSha( - filePath, - commitSha, - LinkedListDirection.PREVIOUS, - forceUpdate - ); - } - ); - - public getFileNextCommitSha = reuseable( - async (filePath: string, commitSha: string): Promise => { - // because we can not find the next commit by GitHub API, - // we can only try to find the next commit from the cache - return this._fileCommitIdListMap - .get(filePath) - ?.getNodeId(commitSha, LinkedListDirection.NEXT); - } - ); - public getFileBlame = reuseable( async (filePath: string, commitSha: string): Promise => { const cacheKey = `${commitSha}:${filePath}`; diff --git a/extensions/github1s/src/repository/types.ts b/extensions/github1s/src/repository/types.ts index 542c3fe80..550e594a4 100644 --- a/extensions/github1s/src/repository/types.ts +++ b/extensions/github1s/src/repository/types.ts @@ -80,3 +80,23 @@ export interface BlameRange { }; }; } + +export interface ObjectManager { + getList(...args: any[]): T[] | Promise; + getItem(...args: any[]): T | Promise; + // return boolean indicate that if there are more results + loadMore(...args: any[]): boolean | Promise; +} + +export interface PullManager extends ObjectManager { + getPullFiles( + ...args: any[] + ): RepositoryChangedFile[] | Promise; +} + +export interface CommitManager extends ObjectManager { + getCommitFiles( + ...args: any[] + ): RepositoryChangedFile[] | Promise; + getFileCommitSha(...args: any[]); +} diff --git a/extensions/github1s/src/router/parser/pull.ts b/extensions/github1s/src/router/parser/pull.ts index b82b90d63..ca3d08736 100644 --- a/extensions/github1s/src/router/parser/pull.ts +++ b/extensions/github1s/src/router/parser/pull.ts @@ -10,7 +10,7 @@ import { RouterState, PageType } from '../types'; export const parsePullUrl = async (path: string): Promise => { const pathParts = parsePath(path).pathname.split('/').filter(Boolean); const [owner, repo, _pageType, pullNumber] = pathParts; - const repositoryPull = await repository.getPull(+pullNumber); + const repositoryPull = await repository.getPullManager().getItem(+pullNumber); return { owner, diff --git a/extensions/github1s/src/router/parser/tree.ts b/extensions/github1s/src/router/parser/tree.ts index e9fbd5c2a..9f8427907 100644 --- a/extensions/github1s/src/router/parser/tree.ts +++ b/extensions/github1s/src/router/parser/tree.ts @@ -28,8 +28,15 @@ const detectRefFormPathParts = async (pathParts: string[]): Promise => { if (!pathParts[3] || pathParts[3].toUpperCase() === 'HEAD') { return 'HEAD'; } - const branchRefs = await repository.getBranches(); - const tagRefs = await repository.getTags(); + // the ref will be pathParts[3] if there is no other parts after it + if (!pathParts[4]) { + return pathParts[3]; + } + // use Promise.all to fetch all refs in parallel as soon as possible + const [branchRefs, tagRefs] = await Promise.all([ + repository.getBranches(), + repository.getTags(), + ]); const refNames = [...branchRefs, ...tagRefs].map((item) => item.name); // fallback to pathParts[3] because it also can be a commit ID return findMatchedBranchOrTag(refNames, pathParts) || pathParts[3]; diff --git a/extensions/github1s/src/source-control/changes.ts b/extensions/github1s/src/source-control/changes.ts index 557118734..5ee8d06d0 100644 --- a/extensions/github1s/src/source-control/changes.ts +++ b/extensions/github1s/src/source-control/changes.ts @@ -35,7 +35,7 @@ export const getPullChangedFiles = async (pull: RepositoryPull) => { const headRootUri = baseRootUri.with({ authority: `${owner}+${repo}+${pull.head.sha}`, }); - const pullFiles = await repository.getPullFiles(pull.number); + const pullFiles = await repository.getPullManager().getPullFiles(pull.number); return pullFiles.map((pullFile) => { // the `previous_filename` field only exists in `RENAMED` file, @@ -64,7 +64,9 @@ export const getCommitChangedFiles = async (commit: RepositoryCommit) => { const headRootUri = baseRootUri.with({ authority: `${owner}+${repo}+${commit.sha || 'HEAD'}`, }); - const commitFiles = await repository.getCommitFiles(commit.sha); + const commitFiles = await repository + .getCommitManager() + .getCommitFiles(commit.sha); return commitFiles.map((commitFile) => { // the `previous_filename` field only exists in `RENAMED` file, @@ -84,12 +86,16 @@ export const getChangedFiles = async (): Promise => { // github pull page if (routerState.pageType === PageType.PULL) { - const pull = await repository.getPull(routerState.pullNumber); + const pull = await repository + .getPullManager() + .getItem(routerState.pullNumber); return pull ? getPullChangedFiles(pull) : []; } // github commit page else if (routerState.pageType === PageType.COMMIT) { - const commit = await repository.getCommit(routerState.commitSha); + const commit = await repository + .getCommitManager() + .getItem(routerState.commitSha); return commit ? getCommitChangedFiles(commit) : []; } return []; diff --git a/extensions/github1s/src/source-control/quickDiffProviders.ts b/extensions/github1s/src/source-control/quickDiffProviders.ts index 76d8cd4ca..98a00bea6 100644 --- a/extensions/github1s/src/source-control/quickDiffProviders.ts +++ b/extensions/github1s/src/source-control/quickDiffProviders.ts @@ -16,7 +16,9 @@ const getOriginalResourceForPull = async ( uri: vscode.Uri, pullNumber: number ): Promise => { - const changedFiles = await repository.getPullFiles(pullNumber); + const changedFiles = await repository + .getPullManager() + .getPullFiles(pullNumber); const changedFile = changedFiles?.find( (changedFile) => changedFile.filename === uri.path.slice(1) ); @@ -29,7 +31,7 @@ const getOriginalResourceForPull = async ( return emptyFileUri; } - const pull = await repository.getPull(pullNumber); + const pull = await repository.getPullManager().getItem(pullNumber); const { owner, repo } = await router.getState(); const originalAuthority = `${owner}+${repo}+${pull.base.sha}`; const originalPath = changedFile.previous_filename @@ -44,7 +46,9 @@ const getOriginalResourceForCommit = async ( uri: vscode.Uri, commitSha: string ) => { - const changedFiles = await repository.getCommitFiles(commitSha); + const changedFiles = await repository + .getCommitManager() + .getCommitFiles(commitSha); const changedFile = changedFiles?.find( (changedFile) => changedFile.filename === uri.path.slice(1) ); @@ -57,7 +61,7 @@ const getOriginalResourceForCommit = async ( return emptyFileUri; } - const commit = await repository.getCommit(commitSha); + const commit = await repository.getCommitManager().getItem(commitSha); const { owner, repo } = await router.getState(); const parentCommitSha = commit.parents?.[0]?.sha; diff --git a/extensions/github1s/src/views/commit-list-view.ts b/extensions/github1s/src/views/commit-list-view.ts index 7b6d4d5f9..acd803d68 100644 --- a/extensions/github1s/src/views/commit-list-view.ts +++ b/extensions/github1s/src/views/commit-list-view.ts @@ -29,6 +29,16 @@ export interface CommitTreeItem extends vscode.TreeItem { commit: RepositoryCommit; } +const loadMoreCommitItem: vscode.TreeItem = { + label: 'Load more', + tooltip: 'Load more commits', + command: { + title: 'Load more commits', + command: 'github1s.commit-view-load-more-commits', + tooltip: 'Load more commits', + }, +}; + export class CommitTreeDataProvider implements vscode.TreeDataProvider { public static viewType = 'github1s.views.commit-list'; @@ -37,21 +47,18 @@ export class CommitTreeDataProvider private _onDidChangeTreeData = new vscode.EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - public updateTree() { - this.forceUpdate = true; + public updateTree(forceUpdate = true) { + this.forceUpdate = forceUpdate; this._onDidChangeTreeData.fire(undefined); } - async getCommitItems(): Promise { + async getCommitItems(): Promise { const { ref } = await router.getState(); - // only recent 100 commits will be list here - // TODO: implement pagination - const repositoryCommits = await repository.getCommits( - ref, - this.forceUpdate - ); + const repositoryCommits = await repository + .getCommitManager() + .getList(ref, this.forceUpdate); this.forceUpdate = false; - return repositoryCommits.map((commit) => { + const commitTreeItems = repositoryCommits.map((commit) => { const label = `${commit.commit.message}`; const description = getCommitTreeItemDescription(commit); const tooltip = `${label} (${description})`; @@ -74,6 +81,9 @@ export class CommitTreeDataProvider collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }; }); + return (await repository.getCommitManager().hasMore()) + ? [...commitTreeItems, loadMoreCommitItem] + : commitTreeItems; } async getCommitFileItems( diff --git a/extensions/github1s/src/views/pull-list-view.ts b/extensions/github1s/src/views/pull-list-view.ts index d296eb2ce..6f1b3a2a5 100644 --- a/extensions/github1s/src/views/pull-list-view.ts +++ b/extensions/github1s/src/views/pull-list-view.ts @@ -67,6 +67,16 @@ export interface PullTreeItem extends vscode.TreeItem { pull: RepositoryPull; } +const loadMorePullItem: vscode.TreeItem = { + label: 'Load more', + tooltip: 'Load more pull requests', + command: { + title: 'Load more pull requests', + command: 'github1s.pull-view-load-more-pulls', + tooltip: 'Load more pull requests', + }, +}; + export class PullRequestTreeDataProvider implements vscode.TreeDataProvider { public static viewType = 'github1s.views.pull-request-list'; @@ -75,17 +85,17 @@ export class PullRequestTreeDataProvider private _onDidChangeTreeData = new vscode.EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - public updateTree() { - this.forceUpdate = true; + public updateTree(forceUpdate = true) { + this.forceUpdate = forceUpdate; this._onDidChangeTreeData.fire(undefined); } - async getPullItems(): Promise { - // only recent 100 pull requests will be list here - // TODO: implement pagination - const repositoryPulls = await repository.getPulls(this.forceUpdate); + async getPullItems(): Promise { + const repositoryPulls = await repository + .getPullManager() + .getList(this.forceUpdate); this.forceUpdate = false; - return repositoryPulls.map((pull) => { + const pullTreeItems = repositoryPulls.map((pull) => { const label = getPullTreeItemLabel(pull); const description = getPullTreeItemDescription(pull); const tooltip = `${label} (${description})`; @@ -106,6 +116,9 @@ export class PullRequestTreeDataProvider collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }; }); + return (await repository.getPullManager().hasMore()) + ? [...pullTreeItems, loadMorePullItem] + : pullTreeItems; } async getPullFileItems(pull: RepositoryPull): Promise {