diff --git a/package.json b/package.json index e02b5127..18f77877 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,13 @@ "command": "git-graph.version", "title": "Get Version Information" }, + { + "category": "Git Graph", + "command": "git-graph.goToCommit", + "title": "Go to Commit", + "icon": "$(git-commit)", + "enablement": "isInDiffEditor || resourceScheme == scm-history-item" + }, { "category": "Git Graph", "command": "git-graph.openFile", @@ -1518,12 +1525,21 @@ }, "menus": { "commandPalette": [ + { + "command": "git-graph.goToCommit", + "when": "isInDiffEditor || resourceScheme == scm-history-item" + }, { "command": "git-graph.openFile", "when": "isInDiffEditor && resourceScheme == git-graph && git-graph:codiconsSupported" } ], "editor/title": [ + { + "command": "git-graph.goToCommit", + "group": "navigation@-150", + "when": "isInDiffEditor || resourceScheme == scm-history-item" + }, { "command": "git-graph.openFile", "group": "navigation", @@ -1541,6 +1557,13 @@ "command": "git-graph.view", "group": "inline" } + ], + "editor/context": [ + { + "command": "git-graph.goToCommit", + "group": "navigation@1", + "when": "isInDiffEditor || resourceScheme == scm-history-item" + } ] } }, diff --git a/src/commands.ts b/src/commands.ts index a6474a68..e4af4031 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -8,7 +8,7 @@ import { CodeReviewData, CodeReviews, ExtensionState } from './extensionState'; import { GitGraphView } from './gitGraphView'; import { Logger } from './logger'; import { RepoManager } from './repoManager'; -import { GitExecutable, UNABLE_TO_FIND_GIT_MSG, VsCodeVersionRequirement, abbrevCommit, abbrevText, copyToClipboard, doesVersionMeetRequirement, getExtensionVersion, getPathFromUri, getRelativeTimeDiff, getRepoName, getSortedRepositoryPaths, isPathInWorkspace, openFile, resolveToSymbolicPath, showErrorMessage, showInformationMessage } from './utils'; +import { GitExecutable, UNABLE_TO_FIND_GIT_MSG, VsCodeVersionRequirement, abbrevCommit, abbrevText, copyToClipboard, doesVersionMeetRequirement, getExtensionVersion, getPathFromUri, getRelativeTimeDiff, getRepoName, getSortedRepositoryPaths, isPathInWorkspace, openFile, resolveToSymbolicPath, showErrorMessage, showInformationMessage, showWarningMessage } from './utils'; import { Disposable } from './utils/disposable'; import { Event } from './utils/event'; @@ -56,6 +56,7 @@ export class CommandManager extends Disposable { this.registerCommand('git-graph.resumeWorkspaceCodeReview', () => this.resumeWorkspaceCodeReview()); this.registerCommand('git-graph.version', () => this.version()); this.registerCommand('git-graph.openFile', (arg) => this.openFile(arg)); + this.registerCommand('git-graph.goToCommit', (arg) => this.goToCommit(arg)); this.registerDisposable( onDidChangeGitExecutable((gitExecutable) => { @@ -345,6 +346,64 @@ export class CommandManager extends Disposable { } } + /** + * Opens a position commit in Git Graph, based on a Git Graph URI (from the Diff View). + * The method run when the `git-graph.goToCommit` command is invoked. + * @param arg The Git Graph URI. + */ + private async goToCommit(arg?: vscode.Uri) { + const uri = arg || vscode.window.activeTextEditor?.document.uri; + const gitExtension = vscode.extensions.getExtension('vscode.git')?.exports; + const api = gitExtension.getAPI(1); + if (!gitExtension) { + showErrorMessage('Unable to load Git extension.'); + return; + } + + if (typeof uri === 'object' && uri) { + let commitHash = ''; + let repository = undefined; + + if (uri.scheme === 'git-graph') { + let diffDocUri = decodeDiffDocUri(uri); + commitHash = diffDocUri.commit; + repository = api.getRepository(diffDocUri.repo); + } + if (uri.scheme === 'git' || uri.scheme === 'gitlens') { + commitHash = JSON.parse(uri.query).ref; + repository = api.getRepository(uri); + } + if (uri.scheme === 'scm-history-item') { + commitHash = uri.path.split('..')[1]; + repository = api.getRepository(uri); + } + + if (commitHash !== '') { + if (!repository) { + showWarningMessage('Warning: no matching Git repository found for this file.'); + } + + if (commitHash.endsWith('^')) { + commitHash = commitHash.slice(0, -1); // it is difficult to find parents, especially when there may be more than one + } + + if (GitGraphView.currentPanel) { // graph exist + GitGraphView.currentPanel.isPanelVisible = true; + await this.view(repository); + GitGraphView.scrollToCommit(commitHash, true, false, true, true); + } else { // graph is creating + await this.view(repository); + GitGraphView.scrollToCommit(commitHash, true, false, true, true); + } + return; + } else { + return showErrorMessage('Unable Go To Commit: The commit hash not found.'); + } + } else { + return showErrorMessage('Unable Go To Commit: The command was not called with the required arguments.'); + } + } + /* Helper Methods */ diff --git a/src/gitGraphView.ts b/src/gitGraphView.ts index 1dd36794..ff7cf771 100644 --- a/src/gitGraphView.ts +++ b/src/gitGraphView.ts @@ -26,7 +26,7 @@ export class GitGraphView extends Disposable { private readonly repoManager: RepoManager; private readonly logger: Logger; private isGraphViewLoaded: boolean = false; - private isPanelVisible: boolean = true; + public isPanelVisible: boolean = true; private currentRepo: string | null = null; private loadViewTo: LoadGitGraphViewTo = null; // Is used by the next call to getHtmlForWebview, and is then reset to null @@ -64,6 +64,20 @@ export class GitGraphView extends Disposable { } } + /** + * Scroll the view to a commit (if it exists). + * @param hash The hash of the commit to scroll to. + * @param alwaysCenterCommit TRUE => Always scroll the view to be centered on the commit. FALSE => Don't scroll the view if the commit is already within the visible portion of commits. + * @param flash Should the commit flash after it has been scrolled to. + * @param openDetails Open details of the specified commit. + * @param persistently Persistently find the commit even if it is not exists. + */ + public static scrollToCommit(hash: string, alwaysCenterCommit: boolean, flash: boolean = false, openDetails: boolean = false, persistently: boolean = false) { + if (GitGraphView.currentPanel) { + GitGraphView.currentPanel.respondScrollToCommit(hash, alwaysCenterCommit, flash, openDetails, persistently); + } + } + /** * Creates a Git Graph View. * @param extensionPath The absolute file path of the directory containing the extension. @@ -818,6 +832,25 @@ export class GitGraphView extends Disposable { loadViewTo: loadViewTo }); } + + /** + * Call the command to scroll to the specified commit to the front-end. + * @param hash The hash of the commit to scroll to. + * @param alwaysCenterCommit TRUE => Always scroll the view to be centered on the commit. FALSE => Don't scroll the view if the commit is already within the visible portion of commits. + * @param flash Should the commit flash after it has been scrolled to. + * @param openDetails Open details of the specified commit. + * @param persistently Persistently find the commit even if it is not exists. + */ + public respondScrollToCommit(hash: string, alwaysCenterCommit: boolean, flash: boolean = false, openDetails: boolean = false, persistently: boolean = false) { + this.sendMessage({ + command: 'scrollToCommit', + hash: hash, + alwaysCenterCommit: alwaysCenterCommit, + flash: flash, + openDetails: openDetails, + persistently: persistently + }); + } } /** diff --git a/src/types.ts b/src/types.ts index 293c1a56..759f8fec 100644 --- a/src/types.ts +++ b/src/types.ts @@ -978,6 +978,15 @@ export interface ResponseLoadRepos extends BaseMessage { readonly loadViewTo: LoadGitGraphViewTo; } +export interface ResponseScrollToCommit extends BaseMessage { + readonly command: 'scrollToCommit'; + readonly hash: string; + readonly alwaysCenterCommit: boolean; + readonly flash: boolean; + readonly openDetails: boolean; + readonly persistently: boolean; +} + export const enum MergeActionOn { Branch = 'Branch', RemoteTrackingBranch = 'Remote-tracking Branch', @@ -1363,6 +1372,7 @@ export type ResponseMessage = | ResponseLoadConfig | ResponseLoadRepoInfo | ResponseLoadRepos + | ResponseScrollToCommit | ResponseMerge | ResponseOpenExtensionSettings | ResponseOpenExternalDirDiff diff --git a/src/utils.ts b/src/utils.ts index 566f0c1f..6ccf7bdf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -543,6 +543,14 @@ export function showInformationMessage(message: string) { return vscode.window.showInformationMessage(message).then(() => { }, () => { }); } +/** + * Show a Visual Studio Code Warning Message Dialog with the specified message. + * @param message The message to show. + */ +export function showWarningMessage(message: string) { + return vscode.window.showWarningMessage(message).then(() => { }, () => { }); +} + /** * Show a Visual Studio Code Error Message Dialog with the specified message. * @param message The message to show. diff --git a/web/main.ts b/web/main.ts index 54976f4a..5c66a5d7 100644 --- a/web/main.ts +++ b/web/main.ts @@ -28,6 +28,14 @@ class GitGraphView { }; private loadViewTo: GG.LoadGitGraphViewTo = null; + public scrollToCommitArgs: { + hash: string, + alwaysCenterCommit: boolean, + flash: boolean, + openDetails: boolean, + persistently: boolean + }; + private readonly graph: Graph; private readonly config: Config; @@ -73,6 +81,14 @@ class GitGraphView { requestingConfig: false }; + this.scrollToCommitArgs = { + hash: '', + alwaysCenterCommit: false, + flash: false, + openDetails: false, + persistently: false + }; + this.controlsElem = document.getElementById('controls')!; this.tableElem = document.getElementById('commitTable')!; this.tableColHeadersElem = document.getElementById('tableColHeaders')!; @@ -165,7 +181,7 @@ class GitGraphView { currentBtn.innerHTML = SVG_ICONS.current; currentBtn.addEventListener('click', () => { if (this.commitHead) { - this.scrollToCommit(this.commitHead, true, true); + this.scrollToCommit(this.commitHead, true, true, false, true); } }); fetchBtn.title = 'Fetch' + (this.config.fetchAndPrune ? ' & Prune' : '') + ' from Remote(s)'; @@ -439,6 +455,10 @@ class GitGraphView { } this.finaliseRepoLoad(true); + + if (this.scrollToCommitArgs.persistently) { + this.scrollToCommit(this.scrollToCommitArgs.hash, this.scrollToCommitArgs.alwaysCenterCommit, this.scrollToCommitArgs.flash, this.scrollToCommitArgs.openDetails, this.scrollToCommitArgs.persistently); + } } private finaliseRepoLoad(didLoadRepoData: boolean) { @@ -581,7 +601,16 @@ class GitGraphView { return options; } public getCommitId(hash: string) { - return typeof this.commitLookup[hash] === 'number' ? this.commitLookup[hash] : null; + if (typeof this.commitLookup[hash] === 'number') { + return this.commitLookup[hash]; + } + // If a full match isn't found, try to find a matching partial hash + for (const key in this.commitLookup) { + if (key.startsWith(hash)) { + return this.commitLookup[key]; + } + } + return null; } private getCommitOfElem(elem: HTMLElement) { @@ -1971,11 +2000,34 @@ class GitGraphView { * @param hash The hash of the commit to scroll to. * @param alwaysCenterCommit TRUE => Always scroll the view to be centered on the commit. FALSE => Don't scroll the view if the commit is already within the visible portion of commits. * @param flash Should the commit flash after it has been scrolled to. + * @param openDetails Open details of the specified commit. + * @param persistently Persistently find the commit even if it is not exists. */ - public scrollToCommit(hash: string, alwaysCenterCommit: boolean, flash: boolean = false) { - const elem = findCommitElemWithId(getCommitElems(), this.getCommitId(hash)); - if (elem === null) return; + public scrollToCommit(hash: string, alwaysCenterCommit: boolean, flash: boolean = false, openDetails: boolean = false, persistently: boolean = false) { + this.scrollToCommitArgs.persistently = false; + const elem = findCommitElemWithId(getCommitElems(), this.getCommitId(hash)); + if (elem === null) { + if (persistently) { + // Scroll to the last loaded commit for trigger loadMoreCommits() + const commits = document.getElementsByClassName('commit'); + if (commits.length === 0) { + return; + } + const lastCommit = commits[commits.length - 1]; + lastCommit.scrollIntoView(); + + this.scrollToCommitArgs = { + hash: hash, + alwaysCenterCommit: alwaysCenterCommit, + flash: flash, + openDetails: openDetails, + persistently: persistently + }; + } + // Do nothing + return; + } let elemTop = this.controlsElem.clientHeight + elem.offsetTop; if (alwaysCenterCommit || elemTop - 8 < this.viewElem.scrollTop || elemTop + 32 - this.viewElem.clientHeight > this.viewElem.scrollTop) { this.viewElem.scroll(0, this.controlsElem.clientHeight + elem.offsetTop + 12 - this.viewElem.clientHeight / 2); @@ -1987,6 +2039,10 @@ class GitGraphView { elem.classList.remove('flash'); }, 850); } + + if (openDetails) { + this.loadCommitDetails(elem); + } } private loadMoreCommits() { @@ -3456,6 +3512,19 @@ window.addEventListener('load', () => { case 'loadRepos': gitGraph.loadRepos(msg.repos, msg.lastActiveRepo, msg.loadViewTo); break; + case 'scrollToCommit': + if (VSCODE_API.getState()?.currentRepoLoading) { // if graph is creating + gitGraph.scrollToCommitArgs = { + hash: msg.hash, + alwaysCenterCommit: msg.alwaysCenterCommit, + flash: msg.flash, + openDetails: msg.openDetails, + persistently: msg.persistently + }; + } else { // if graph exist + gitGraph.scrollToCommit(msg.hash, msg.alwaysCenterCommit, msg.flash, msg.openDetails, msg.persistently); + } + break; case 'merge': refreshOrDisplayError(msg.error, 'Unable to Merge ' + msg.actionOn); break;