diff --git a/CHANGELOG.md b/CHANGELOG.md index 6143e10c..21f6bec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 1.3.1 - 2019-02-17 * View the Visual Studio Code Diff of a file change in a commit, by clicking on the file in the commit details view. +* All git commands are run asynchronously to improve responsiveness. ## 1.3.0 - 2019-02-16 * Commit details view (click on a commit to open it). This shows the full commit details, and a tree view of all file changes in the commit. diff --git a/src/dataSource.ts b/src/dataSource.ts index fc1375f7..c2df95cf 100644 --- a/src/dataSource.ts +++ b/src/dataSource.ts @@ -1,6 +1,6 @@ import * as cp from 'child_process'; import { Config } from './config'; -import { GitCommandStatus, GitCommit, GitCommitDetails, GitCommitNode, GitFileChangeType, GitRef, GitResetMode, GitUnsavedChanges } from './types'; +import { GitCommandStatus, GitCommit, GitCommitDetails, GitCommitNode, GitFileChangeType, GitRef, GitResetMode, GitUnsavedChangesCmdResp } from './types'; const eolRegex = /\r\n|\r|\n/g; const gitLogSeparator = 'XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb'; @@ -14,38 +14,41 @@ export class DataSource { this.workspaceDir = workspaceDir; } - public isGitRepository(): boolean { - try { - cp.execSync('git rev-parse --git-dir', { cwd: this.workspaceDir }); - return true; - } catch (e) { - return false; - } + public isGitRepository() { + return new Promise((resolve) => { + cp.exec('git rev-parse --git-dir', { cwd: this.workspaceDir }, (err) => { + resolve(!err); + }); + }); } - public getBranches(showRemoteBranches: boolean): string[] { - try { - let lines = cp.execSync('git branch' + (showRemoteBranches ? ' -a' : ''), { cwd: this.workspaceDir }).toString().split(eolRegex); - let branches: string[] = []; - for (let i = 0; i < lines.length - 1; i++) { - let active = lines[i][0] === '*'; - let name = lines[i].substring(2).split(' ')[0]; - if (active) { - branches.unshift(name); + public getBranches(showRemoteBranches: boolean) { + return new Promise((resolve) => { + cp.exec('git branch' + (showRemoteBranches ? ' -a' : ''), { cwd: this.workspaceDir }, (err, stdout) => { + if (!err) { + let lines = stdout.split(eolRegex); + let branches: string[] = []; + for (let i = 0; i < lines.length - 1; i++) { + let active = lines[i][0] === '*'; + let name = lines[i].substring(2).split(' ')[0]; + if (active) { + branches.unshift(name); + } else { + branches.push(name); + } + } + resolve(branches); } else { - branches.push(name); + resolve([]); } - } - return branches; - } catch (e) { - return []; - } + }); + }); } - public getCommits(branch: string, maxCommits: number, showRemoteBranches: boolean, currentBranch: string | null) { + public async getCommits(branch: string, maxCommits: number, showRemoteBranches: boolean, currentBranch: string | null) { let i, j; - let commits = this.getGitLog(branch, maxCommits + 1, showRemoteBranches); - let refs = this.getRefs(showRemoteBranches); + let commits = await this.getGitLog(branch, maxCommits + 1, showRemoteBranches); + let refs = await this.getRefs(showRemoteBranches); let unsavedChanges = null; let moreCommitsAvailable = commits.length === maxCommits + 1; @@ -59,7 +62,7 @@ export class DataSource { } } if (currentBranchHash !== null && (branch === '' || branch === currentBranch)) { - unsavedChanges = (new Config()).showUncommittedChanges() ? this.getGitUnsavedChanges() : null; + unsavedChanges = (new Config()).showUncommittedChanges() ? await this.getGitUnsavedChanges() : null; if (unsavedChanges !== null) { for (j = 0; j < commits.length; j++) { if (currentBranchHash === commits[j].hash) { @@ -99,136 +102,181 @@ export class DataSource { return { commits: commitNodes, moreCommitsAvailable: moreCommitsAvailable }; } - public commitDetails(commitHash: string) { + public async commitDetails(commitHash: string) { try { - let lines = cp.execSync('git show --quiet ' + commitHash + ' --format="' + gitCommitDetailsFormat + '"', { cwd: this.workspaceDir }).toString().split(eolRegex); - let commitInfo = lines[0].split(gitLogSeparator); - let details: GitCommitDetails = { - hash: commitInfo[0], - parents: commitInfo[1].split(' '), - author: commitInfo[2], - email: commitInfo[3], - date: parseInt(commitInfo[4]), - committer: commitInfo[5], - body: commitInfo[6], - fileChanges: [] - }; - + let details = await new Promise((resolve, reject) => { + cp.exec('git show --quiet ' + commitHash + ' --format="' + gitCommitDetailsFormat + '"', { cwd: this.workspaceDir }, (err, stdout) => { + if (!err) { + let lines = stdout.split(eolRegex); + let commitInfo = lines[0].split(gitLogSeparator); + resolve({ + hash: commitInfo[0], + parents: commitInfo[1].split(' '), + author: commitInfo[2], + email: commitInfo[3], + date: parseInt(commitInfo[4]), + committer: commitInfo[5], + body: commitInfo[6], + fileChanges: [] + }); + } else { + reject(); + } + }); + }); let fileLookup: { [file: string]: number } = {}; - lines = cp.execSync('git diff-tree --name-status -r -m --root --find-renames --diff-filter=AMDR ' + commitHash, { cwd: this.workspaceDir }).toString().split(eolRegex); - for (let i = 1; i < lines.length - 1; i++) { - let line = lines[i].split('\t'); - if (line.length < 2) break; - let oldFilePath = line[1].replace(/\\/g, '/'), newFilePath = line[line.length-1].replace(/\\/g, '/'); - fileLookup[newFilePath] = details.fileChanges.length; - details.fileChanges.push({ oldFilePath: oldFilePath, newFilePath: newFilePath, type: line[0][0], additions: null, deletions: null }); - } - lines = cp.execSync('git diff-tree --numstat -r -m --root --find-renames --diff-filter=AMDR ' + commitHash, { cwd: this.workspaceDir }).toString().split(eolRegex); - for (let i = 1; i < lines.length - 1; i++) { - let line = lines[i].split('\t'); - if (line.length !== 3) break; - let fileName = line[2].replace(/(.*){.* => (.*)}/, '$1$2').replace(/.* => (.*)/, '$1'); - if (typeof fileLookup[fileName] === 'number') { - details.fileChanges[fileLookup[fileName]].additions = parseInt(line[0]); - details.fileChanges[fileLookup[fileName]].deletions = parseInt(line[1]); - } - } + await new Promise((resolve, reject) => { + cp.exec('git diff-tree --name-status -r -m --root --find-renames --diff-filter=AMDR ' + commitHash, { cwd: this.workspaceDir }, (err, stdout) => { + if (!err) { + let lines = stdout.split(eolRegex); + for (let i = 1; i < lines.length - 1; i++) { + let line = lines[i].split('\t'); + if (line.length < 2) break; + let oldFilePath = line[1].replace(/\\/g, '/'), newFilePath = line[line.length - 1].replace(/\\/g, '/'); + fileLookup[newFilePath] = details.fileChanges.length; + details.fileChanges.push({ oldFilePath: oldFilePath, newFilePath: newFilePath, type: line[0][0], additions: null, deletions: null }); + } + resolve(); + } else { + reject(); + } + }); + }); + await new Promise((resolve, reject) => { + cp.exec('git diff-tree --numstat -r -m --root --find-renames --diff-filter=AMDR ' + commitHash, { cwd: this.workspaceDir }, (err, stdout) => { + if (!err) { + let lines = stdout.split(eolRegex); + for (let i = 1; i < lines.length - 1; i++) { + let line = lines[i].split('\t'); + if (line.length !== 3) break; + let fileName = line[2].replace(/(.*){.* => (.*)}/, '$1$2').replace(/.* => (.*)/, '$1'); + if (typeof fileLookup[fileName] === 'number') { + details.fileChanges[fileLookup[fileName]].additions = parseInt(line[0]); + details.fileChanges[fileLookup[fileName]].deletions = parseInt(line[1]); + } + } + resolve(); + } else { + reject(); + } + + }); + }); return details; } catch (e) { return null; } } - public getFile(commitHash: string, filePath: string) { - try { - return cp.execSync('git show "' + commitHash + '":"' + filePath + '"', { cwd: this.workspaceDir }).toString(); - } catch (e) { - return ''; - } + public async getCommitFile(commitHash: string, filePath: string) { + return new Promise((resolve) => { + cp.exec('git show "' + commitHash + '":"' + filePath + '"', { cwd: this.workspaceDir }, (err, stdout) => { + resolve(!err ? stdout : ''); + }); + }); } - public addTag(tagName: string, commitHash: string): GitCommandStatus { + public addTag(tagName: string, commitHash: string) { return this.runGitCommand('git tag -a ' + escapeRefName(tagName) + ' -m "" ' + commitHash); } - public deleteTag(tagName: string): GitCommandStatus { + public deleteTag(tagName: string) { return this.runGitCommand('git tag -d ' + escapeRefName(tagName)); } - public createBranch(branchName: string, commitHash: string): GitCommandStatus { + public createBranch(branchName: string, commitHash: string) { return this.runGitCommand('git branch ' + escapeRefName(branchName) + ' ' + commitHash); } - public checkoutBranch(branchName: string, remoteBranch: string | null): GitCommandStatus { + public checkoutBranch(branchName: string, remoteBranch: string | null) { return this.runGitCommand('git checkout ' + (remoteBranch === null ? escapeRefName(branchName) : ' -b ' + escapeRefName(branchName) + ' ' + escapeRefName(remoteBranch))); } - public deleteBranch(branchName: string, forceDelete: boolean): GitCommandStatus { + public deleteBranch(branchName: string, forceDelete: boolean) { return this.runGitCommand('git branch --delete' + (forceDelete ? ' --force' : '') + ' ' + escapeRefName(branchName)); } - public renameBranch(oldName: string, newName: string): GitCommandStatus { + public renameBranch(oldName: string, newName: string) { return this.runGitCommand('git branch -m ' + escapeRefName(oldName) + ' ' + escapeRefName(newName)); } - public resetToCommit(commitHash: string, resetMode: GitResetMode): GitCommandStatus { + public resetToCommit(commitHash: string, resetMode: GitResetMode) { return this.runGitCommand('git reset --' + resetMode + ' ' + commitHash); } - private runGitCommand(command: string): GitCommandStatus { - try { - cp.execSync(command, { cwd: this.workspaceDir }); - return null; - } catch (e) { - let lines = e.message.split(eolRegex); - return lines.slice(1, lines.length - 1).join('\n'); - } + private async runGitCommand(command: string) { + return new Promise((resolve) => { + cp.exec(command, { cwd: this.workspaceDir }, (err) => { + if (!err) { + resolve(null); + } else { + let lines = err.message.split(eolRegex); + resolve(lines.slice(1, lines.length - 1).join('\n')); + } + }); + }); } - private getRefs(showRemoteBranches: boolean): GitRef[] { - try { - let lines = cp.execSync('git show-ref ' + (showRemoteBranches ? '' : '--heads --tags') + ' -d', { cwd: this.workspaceDir }).toString().split(eolRegex); - let refs: GitRef[] = []; - for (let i = 0; i < lines.length - 1; i++) { - let line = lines[i].split(' '); - if (line.length < 2) continue; - - let hash = line.shift()!; - let ref = line.join(' '); - - if (ref.startsWith('refs/heads/')) { - refs.push({ hash: hash, name: ref.substring(11), type: 'head' }); - } else if (ref.startsWith('refs/tags/')) { - refs.push({ hash: hash, name: (ref.endsWith('^{}') ? ref.substring(10, ref.length - 3) : ref.substring(10)), type: 'tag' }); - } else if (ref.startsWith('refs/remotes/')) { - refs.push({ hash: hash, name: ref.substring(13), type: 'remote' }); + private async getRefs(showRemoteBranches: boolean) { + return new Promise((resolve) => { + cp.exec('git show-ref ' + (showRemoteBranches ? '' : '--heads --tags') + ' -d', { cwd: this.workspaceDir }, (err, stdout) => { + if (!err) { + let lines = stdout.split(eolRegex); + let refs: GitRef[] = []; + for (let i = 0; i < lines.length - 1; i++) { + let line = lines[i].split(' '); + if (line.length < 2) continue; + + let hash = line.shift()!; + let ref = line.join(' '); + + if (ref.startsWith('refs/heads/')) { + refs.push({ hash: hash, name: ref.substring(11), type: 'head' }); + } else if (ref.startsWith('refs/tags/')) { + refs.push({ hash: hash, name: (ref.endsWith('^{}') ? ref.substring(10, ref.length - 3) : ref.substring(10)), type: 'tag' }); + } else if (ref.startsWith('refs/remotes/')) { + refs.push({ hash: hash, name: ref.substring(13), type: 'remote' }); + } + } + resolve(refs); + } else { + resolve([]); } - } - return refs; - } catch (e) { - return []; - } + }); + }); } - private getGitLog(branch: string, num: number, showRemoteBranches: boolean): GitCommit[] { - try { - let lines = cp.execSync('git log ' + (branch !== '' ? escapeRefName(branch) : '--branches' + (showRemoteBranches ? ' --remotes' : '')) + ' --max-count=' + num + ' --format="' + gitLogFormat + '"', { cwd: this.workspaceDir }).toString().split(eolRegex); - let gitCommits: GitCommit[] = []; - for (let i = 0; i < lines.length - 1; i++) { - let line = lines[i].split(gitLogSeparator); - if (line.length !== 6) break; - gitCommits.push({ hash: line[0], parentHashes: line[1].split(' '), author: line[2], email: line[3], date: parseInt(line[4]), message: line[5] }); - } - return gitCommits; - } catch (e) { - return []; - } + private async getGitLog(branch: string, num: number, showRemoteBranches: boolean) { + return new Promise((resolve) => { + cp.exec('git log ' + (branch !== '' ? escapeRefName(branch) : '--branches' + (showRemoteBranches ? ' --remotes' : '')) + ' --max-count=' + num + ' --format="' + gitLogFormat + '"', { cwd: this.workspaceDir }, (err, stdout) => { + if (!err) { + let lines = stdout.split(eolRegex); + let gitCommits: GitCommit[] = []; + for (let i = 0; i < lines.length - 1; i++) { + let line = lines[i].split(gitLogSeparator); + if (line.length !== 6) break; + gitCommits.push({ hash: line[0], parentHashes: line[1].split(' '), author: line[2], email: line[3], date: parseInt(line[4]), message: line[5] }); + } + resolve(gitCommits); + } else { + resolve([]); + } + }); + }); } - private getGitUnsavedChanges(): GitUnsavedChanges | null { + private getGitUnsavedChanges() { try { - let lines = cp.execSync('git status -s --branch --untracked-files --porcelain', { cwd: this.workspaceDir }).toString().split(eolRegex); - return lines.length > 2 ? { branch: lines[0].substring(3).split('...')[0], changes: lines.length - 2 } : null; + return new Promise((resolve, reject) => { + cp.exec('git status -s --branch --untracked-files --porcelain', { cwd: this.workspaceDir }, (err, stdout) => { + if (!err) { + let lines = stdout.split(eolRegex); + resolve(lines.length > 2 ? { branch: lines[0].substring(3).split('...')[0], changes: lines.length - 2 } : null); + } else { + reject(); + } + }); + }); } catch (e) { return null; } diff --git a/src/diffDocProvider.ts b/src/diffDocProvider.ts index 8fed7645..6268431f 100644 --- a/src/diffDocProvider.ts +++ b/src/diffDocProvider.ts @@ -25,14 +25,15 @@ export class DiffDocProvider implements vscode.TextDocumentContentProvider { public provideTextDocumentContent(uri: vscode.Uri): string | Thenable { let document = this.docs.get(uri.toString()); - if (document) { - return document.value; - } + if (document) return document.value; + if (this.dataSource === null) return ''; let request = decodeDiffDocUri(uri); - document = new DiffDocument(this.dataSource !== null ? this.dataSource.getFile(request.commit, request.filePath) : ''); - this.docs.set(uri.toString(), document); - return document.value; + return this.dataSource.getCommitFile(request.commit, request.filePath).then((data) => { + let document = new DiffDocument(data); + this.docs.set(uri.toString(), document); + return document.value; + }); } } @@ -49,9 +50,9 @@ class DiffDocument { } export function encodeDiffDocUri(path: string, commit: string): vscode.Uri { - return vscode.Uri.parse(DiffDocProvider.scheme + ':'+path.replace(/\\/g, '/')+'?'+commit); + return vscode.Uri.parse(DiffDocProvider.scheme + ':' + path.replace(/\\/g, '/') + '?' + commit); } export function decodeDiffDocUri(uri: vscode.Uri) { - return {filePath: uri.path, commit:uri.query}; + return { filePath: uri.path, commit: uri.query }; } diff --git a/src/gitGraphView.ts b/src/gitGraphView.ts index 53cddca5..c64ba297 100644 --- a/src/gitGraphView.ts +++ b/src/gitGraphView.ts @@ -51,25 +51,25 @@ export class GitGraphView { case 'loadBranches': this.sendMessage({ command: 'loadBranches', - data: this.dataSource.getBranches(message.data.showRemoteBranches) + data: await this.dataSource.getBranches(message.data.showRemoteBranches) }); return; case 'loadCommits': this.sendMessage({ command: 'loadCommits', - data: this.dataSource.getCommits(message.data.branch, message.data.maxCommits, message.data.showRemoteBranches, message.data.currentBranch) + data: await this.dataSource.getCommits(message.data.branch, message.data.maxCommits, message.data.showRemoteBranches, message.data.currentBranch) }); return; case 'addTag': this.sendMessage({ command: 'addTag', - data: this.dataSource.addTag(message.data.tagName, message.data.commitHash) + data: await this.dataSource.addTag(message.data.tagName, message.data.commitHash) }); return; case 'deleteTag': this.sendMessage({ command: 'deleteTag', - data: this.dataSource.deleteTag(message.data) + data: await this.dataSource.deleteTag(message.data) }); return; case 'copyCommitHashToClipboard': @@ -78,37 +78,37 @@ export class GitGraphView { case 'createBranch': this.sendMessage({ command: 'createBranch', - data: this.dataSource.createBranch(message.data.branchName, message.data.commitHash) + data: await this.dataSource.createBranch(message.data.branchName, message.data.commitHash) }); return; case 'checkoutBranch': this.sendMessage({ command: 'checkoutBranch', - data: this.dataSource.checkoutBranch(message.data.branchName, message.data.remoteBranch) + data: await this.dataSource.checkoutBranch(message.data.branchName, message.data.remoteBranch) }); return; case 'deleteBranch': this.sendMessage({ command: 'deleteBranch', - data: this.dataSource.deleteBranch(message.data.branchName, message.data.forceDelete) + data: await this.dataSource.deleteBranch(message.data.branchName, message.data.forceDelete) }); return; case 'renameBranch': this.sendMessage({ command: 'renameBranch', - data: this.dataSource.renameBranch(message.data.oldName, message.data.newName) + data: await this.dataSource.renameBranch(message.data.oldName, message.data.newName) }); return; case 'resetToCommit': this.sendMessage({ command: 'resetToCommit', - data: this.dataSource.resetToCommit(message.data.commitHash, message.data.resetMode) + data: await this.dataSource.resetToCommit(message.data.commitHash, message.data.resetMode) }); return; case 'commitDetails': this.sendMessage({ command: 'commitDetails', - data: this.dataSource.commitDetails(message.data) + data: await this.dataSource.commitDetails(message.data) }); return; case 'viewDiff': @@ -129,17 +129,17 @@ export class GitGraphView { } } - private update() { - this.panel.webview.html = this.getHtmlForWebview(); + private async update() { + this.panel.webview.html = await this.getHtmlForWebview(); } - private getHtmlForWebview() { + private async getHtmlForWebview() { const config = new Config(); const jsPathOnDisk = vscode.Uri.file(path.join(this.extensionPath, 'media', 'main.js')); const jsUri = jsPathOnDisk.with({ scheme: 'vscode-resource' }); const cssPathOnDisk = vscode.Uri.file(path.join(this.extensionPath, 'media', 'main.css')); const cssUri = cssPathOnDisk.with({ scheme: 'vscode-resource' }); - const isRepo = this.dataSource !== null && this.dataSource.isGitRepository(); + const isRepo = this.dataSource !== null && await this.dataSource.isGitRepository(); const nonce = getNonce(); let settings: GitGraphViewSettings = { @@ -203,6 +203,7 @@ export class GitGraphView { let pathComponents = newFilePath.split('/'); let title = pathComponents[pathComponents.length - 1] + ' (' + (type === 'A' ? 'Added in ' + abbrevHash : type === 'D' ? 'Deleted in ' + abbrevHash : abbrevCommit(commitHash) + '^ ↔ ' + abbrevCommit(commitHash)) + ')'; vscode.commands.executeCommand('vscode.diff', encodeDiffDocUri(oldFilePath, commitHash + '^'), encodeDiffDocUri(newFilePath, commitHash), title, { preview: true }); + this.sendMessage({ command: 'viewDiff', data: true }); } } diff --git a/src/types.ts b/src/types.ts index 94e8fbee..45aa18e7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -234,3 +234,4 @@ export type GraphStyle = 'rounded' | 'angular'; export type GitCommandStatus = string | null; export type GitResetMode = 'soft' | 'mixed' | 'hard'; export type GitFileChangeType = 'A' | 'M' | 'D' | 'R'; +export type GitUnsavedChangesCmdResp = GitUnsavedChanges | null; diff --git a/web/main.ts b/web/main.ts index 62c9ed28..6705c7a5 100644 --- a/web/main.ts +++ b/web/main.ts @@ -362,6 +362,7 @@ vscode.setState({ branchOptions: this.branchOptions, commits: this.commits, + moreCommitsAvailable: this.moreCommitsAvailable, selectedBranch: this.selectedBranch, maxCommits: this.maxCommits, showRemoteBranches: this.showRemoteBranches,