diff --git a/src/commands/showQuickFileHistory.ts b/src/commands/showQuickFileHistory.ts index 79a7229..f6dde4a 100644 --- a/src/commands/showQuickFileHistory.ts +++ b/src/commands/showQuickFileHistory.ts @@ -3,7 +3,7 @@ import { commands, TextEditor, Uri, window } from 'vscode'; import { ActiveEditorCommand, Commands } from './commands'; import { GitProvider, GitUri, IGitLog } from '../gitProvider'; import { Logger } from '../logger'; -import { CommandQuickPickItem, FileHistoryQuickPick } from '../quickPicks'; +import { CommandQuickPickItem, FileHistoryQuickPick, showQuickPickProgress } from '../quickPicks'; import * as path from 'path'; export class ShowQuickFileHistoryCommand extends ActiveEditorCommand { @@ -27,13 +27,16 @@ export class ShowQuickFileHistoryCommand extends ActiveEditorCommand { maxCount = this.git.config.advanced.maxQuickHistory; } + const progressCancellation = showQuickPickProgress(`Loading file history \u2014 ${maxCount ? ` limited to ${maxCount} commits` : ` this may take a while`}\u2026`); try { if (!log) { log = await this.git.getLogForFile(gitUri.fsPath, gitUri.sha, gitUri.repoPath, undefined, maxCount); if (!log) return window.showWarningMessage(`Unable to show file history. File is probably not under source control`); } - const pick = await FileHistoryQuickPick.show(log, uri, gitUri.sha, maxCount, this.git.config.advanced.maxQuickHistory, goBackCommand); + if (progressCancellation.token.isCancellationRequested) return undefined; + + const pick = await FileHistoryQuickPick.show(log, uri, gitUri.sha, progressCancellation, goBackCommand); if (!pick) return undefined; if (pick instanceof CommandQuickPickItem) { @@ -52,5 +55,8 @@ export class ShowQuickFileHistoryCommand extends ActiveEditorCommand { Logger.error('[GitLens.ShowQuickFileHistoryCommand]', 'getLogLocations', ex); return window.showErrorMessage(`Unable to show file history. See output channel for more details`); } + finally { + progressCancellation.dispose(); + } } } \ No newline at end of file diff --git a/src/commands/showQuickRepoHistory.ts b/src/commands/showQuickRepoHistory.ts index fcb3960..8d6997d 100644 --- a/src/commands/showQuickRepoHistory.ts +++ b/src/commands/showQuickRepoHistory.ts @@ -3,7 +3,7 @@ import { commands, TextEditor, Uri, window } from 'vscode'; import { ActiveEditorCommand, Commands } from './commands'; import { GitProvider, GitUri, IGitLog } from '../gitProvider'; import { Logger } from '../logger'; -import { CommandQuickPickItem, RepoHistoryQuickPick } from '../quickPicks'; +import { CommandQuickPickItem, RepoHistoryQuickPick, showQuickPickProgress } from '../quickPicks'; export class ShowQuickRepoHistoryCommand extends ActiveEditorCommand { @@ -11,7 +11,7 @@ export class ShowQuickRepoHistoryCommand extends ActiveEditorCommand { super(Commands.ShowQuickRepoHistory); } - async execute(editor: TextEditor, uri?: Uri, maxCount?: number, goBackCommand?: CommandQuickPickItem, log?: IGitLog) { + async execute(editor: TextEditor, uri?: Uri, sha?: string, maxCount?: number | undefined, goBackCommand?: CommandQuickPickItem, log?: IGitLog) { if (!(uri instanceof Uri)) { uri = editor && editor.document && editor.document.uri; } @@ -20,16 +20,21 @@ export class ShowQuickRepoHistoryCommand extends ActiveEditorCommand { maxCount = this.git.config.advanced.maxQuickHistory; } + const progressCancellation = showQuickPickProgress(`Loading repository history \u2014 ${maxCount ? ` limited to ${maxCount} commits` : ` this may take a while`}\u2026`); try { if (!log) { const repoPath = await this.git.getRepoPathFromUri(uri, this.repoPath); if (!repoPath) return window.showWarningMessage(`Unable to show repository history`); - log = await this.git.getLogForRepo(repoPath, undefined, maxCount); + if (progressCancellation.token.isCancellationRequested) return undefined; + + log = await this.git.getLogForRepo(repoPath, sha, maxCount); if (!log) return window.showWarningMessage(`Unable to show repository history`); } - const pick = await RepoHistoryQuickPick.show(log, uri, maxCount, this.git.config.advanced.maxQuickHistory, goBackCommand); + if (progressCancellation.token.isCancellationRequested) return undefined; + + const pick = await RepoHistoryQuickPick.show(log, uri, sha, progressCancellation, goBackCommand); if (!pick) return undefined; if (pick instanceof CommandQuickPickItem) { @@ -40,12 +45,15 @@ export class ShowQuickRepoHistoryCommand extends ActiveEditorCommand { new CommandQuickPickItem({ label: `go back \u21A9`, description: `\u00a0 \u2014 \u00a0\u00a0 to repository history` - }, Commands.ShowQuickRepoHistory, [uri, maxCount, goBackCommand, log]), + }, Commands.ShowQuickRepoHistory, [uri, sha, maxCount, goBackCommand, log]), log); } catch (ex) { Logger.error('[GitLens.ShowQuickRepoHistoryCommand]', ex); return window.showErrorMessage(`Unable to show repository history. See output channel for more details`); } + finally { + progressCancellation.dispose(); + } } } \ No newline at end of file diff --git a/src/git/enrichers/blameParserEnricher.ts b/src/git/enrichers/blameParserEnricher.ts index c06852e..ef5f6c0 100644 --- a/src/git/enrichers/blameParserEnricher.ts +++ b/src/git/enrichers/blameParserEnricher.ts @@ -1,5 +1,5 @@ 'use strict'; -import Git, { GitBlameFormat, GitCommit, IGitAuthor, IGitBlame, IGitCommitLine, IGitEnricher } from './../git'; +import { Git, GitBlameFormat, GitCommit, IGitAuthor, IGitBlame, IGitCommitLine, IGitEnricher } from './../git'; import * as moment from 'moment'; import * as path from 'path'; diff --git a/src/git/enrichers/logParserEnricher.ts b/src/git/enrichers/logParserEnricher.ts index 3556334..37c5f44 100644 --- a/src/git/enrichers/logParserEnricher.ts +++ b/src/git/enrichers/logParserEnricher.ts @@ -1,5 +1,6 @@ 'use strict'; -import Git, { GitFileStatus, GitLogCommit, GitLogType, IGitAuthor, IGitEnricher, IGitLog } from './../git'; +import { Git, GitFileStatus, GitLogCommit, GitLogType, IGitAuthor, IGitEnricher, IGitLog } from './../git'; +// import { Logger } from '../../logger'; import * as moment from 'moment'; import * as path from 'path'; @@ -151,7 +152,7 @@ export class GitLogParserEnricher implements IGitEnricher { return entries; } - enrich(data: string, type: GitLogType, fileNameOrRepoPath: string, isRepoPath: boolean = false): IGitLog { + enrich(data: string, type: GitLogType, fileNameOrRepoPath: string, maxCount: number | undefined, isRepoPath: boolean = false): IGitLog { const entries = this._parseEntries(data, isRepoPath); if (!entries) return undefined; @@ -199,6 +200,9 @@ export class GitLogParserEnricher implements IGitEnricher { commits.set(entry.sha, commit); } + // else { + // Logger.log(`merge commit? ${entry.sha}`); + // } if (recentCommit) { recentCommit.previousSha = commit.sha; @@ -230,7 +234,9 @@ export class GitLogParserEnricher implements IGitEnricher { repoPath: repoPath, authors: sortedAuthors, // commits: sortedCommits, - commits: commits + commits: commits, + maxCount: maxCount, + truncated: maxCount && entries.length >= maxCount } as IGitLog; } } \ No newline at end of file diff --git a/src/git/git.ts b/src/git/git.ts index 2552c24..eb316d7 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -40,7 +40,7 @@ export const GitBlameFormat = { porcelain: '--porcelain' as GitBlameFormat }; -export default class Git { +export class Git { static normalizePath(fileName: string, repoPath?: string) { return fileName.replace(/\\/g, '/'); diff --git a/src/git/gitEnrichment.ts b/src/git/gitEnrichment.ts index 7b34380..3326c9e 100644 --- a/src/git/gitEnrichment.ts +++ b/src/git/gitEnrichment.ts @@ -1,6 +1,6 @@ 'use strict'; import { Uri } from 'vscode'; -import Git from './git'; +import { Git } from './git'; import * as path from 'path'; export interface IGitEnricher { @@ -171,6 +171,9 @@ export interface IGitLog { repoPath: string; authors: Map; commits: Map; + + maxCount: number | undefined; + truncated: boolean; } export declare type GitFileStatus = '?' | 'A' | 'C' | 'D' | 'M' | 'R' | 'U'; diff --git a/src/gitProvider.ts b/src/gitProvider.ts index 2855d70..25d8f09 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -4,7 +4,7 @@ import { Disposable, Event, EventEmitter, ExtensionContext, FileSystemWatcher, l import { CommandContext, setCommandContext } from './commands'; import { CodeLensVisibility, IConfig } from './configuration'; import { DocumentSchemes, WorkspaceState } from './constants'; -import Git, { GitBlameParserEnricher, GitBlameFormat, GitCommit, GitFileStatusItem, GitLogParserEnricher, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitLog } from './git/git'; +import { Git, GitBlameParserEnricher, GitBlameFormat, GitCommit, GitFileStatusItem, GitLogParserEnricher, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitLog } from './git/git'; import { IGitUriData, GitUri } from './git/gitUri'; import GitCodeLensProvider from './gitCodeLensProvider'; import { Logger } from './logger'; @@ -498,7 +498,7 @@ export class GitProvider extends Disposable { try { const data = await Git.logRepo(repoPath, sha, maxCount); - return new GitLogParserEnricher().enrich(data, 'repo', repoPath, true); + return new GitLogParserEnricher().enrich(data, 'repo', repoPath, maxCount, true); } catch (ex) { return undefined; @@ -532,7 +532,7 @@ export class GitProvider extends Disposable { return (range ? Git.logRange(fileName, range.start.line + 1, range.end.line + 1, sha, repoPath, maxCount) : Git.log(fileName, sha, repoPath, maxCount, reverse)) - .then(data => new GitLogParserEnricher().enrich(data, 'file', repoPath || fileName, !!repoPath)) + .then(data => new GitLogParserEnricher().enrich(data, 'file', repoPath || fileName, maxCount, !!repoPath)) .catch(ex => { // Trap and cache expected log errors if (useCaching) { diff --git a/src/quickPicks.ts b/src/quickPicks.ts index f42e11f..6e4795a 100644 --- a/src/quickPicks.ts +++ b/src/quickPicks.ts @@ -1,8 +1,8 @@ 'use strict'; -export { CommandQuickPickItem, OpenFileCommandQuickPickItem, OpenFilesCommandQuickPickItem } from './quickPicks/quickPicks'; +export { CommandQuickPickItem, OpenFileCommandQuickPickItem, OpenFilesCommandQuickPickItem, showQuickPickProgress } from './quickPicks/quickPicks'; export { CommitQuickPickItem, CommitWithFileStatusQuickPickItem } from './quickPicks/gitQuickPicks'; export { OpenCommitFilesCommandQuickPickItem, OpenCommitWorkingTreeFilesCommandQuickPickItem, CommitDetailsQuickPick } from './quickPicks/commitDetails'; export { OpenCommitFileCommandQuickPickItem, OpenCommitWorkingTreeFileCommandQuickPickItem, CommitFileDetailsQuickPick } from './quickPicks/commitFileDetails'; export { FileHistoryQuickPick } from './quickPicks/fileHistory'; export { RepoHistoryQuickPick } from './quickPicks/repoHistory'; -export { OpenStatusFileCommandQuickPickItem, OpenStatusFilesCommandQuickPickItem, RepoStatusQuickPick } from './quickPicks/repoStatus'; +export { OpenStatusFileCommandQuickPickItem, OpenStatusFilesCommandQuickPickItem, RepoStatusQuickPick } from './quickPicks/repoStatus'; \ No newline at end of file diff --git a/src/quickPicks/fileHistory.ts b/src/quickPicks/fileHistory.ts index 7e5fd62..07ebef0 100644 --- a/src/quickPicks/fileHistory.ts +++ b/src/quickPicks/fileHistory.ts @@ -1,23 +1,29 @@ 'use strict'; import { Iterables } from '../system'; -import { QuickPickItem, QuickPickOptions, Uri, window } from 'vscode'; +import { CancellationTokenSource, QuickPickItem, QuickPickOptions, Uri, window } from 'vscode'; import { Commands, Keyboard } from '../commands'; -import { IGitLog } from '../gitProvider'; +import { GitUri, IGitLog } from '../gitProvider'; import { CommitQuickPickItem } from './gitQuickPicks'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut } from './quickPicks'; import * as path from 'path'; export class FileHistoryQuickPick { - static async show(log: IGitLog, uri: Uri, sha: string, maxCount: number, defaultMaxCount: number, goBackCommand?: CommandQuickPickItem): Promise { + static async show(log: IGitLog, uri: Uri, sha: string, progressCancellation: CancellationTokenSource, goBackCommand?: CommandQuickPickItem): Promise { const items = Array.from(Iterables.map(log.commits.values(), c => new CommitQuickPickItem(c))) as (CommitQuickPickItem | CommandQuickPickItem)[]; - if (maxCount !== 0 && items.length >= defaultMaxCount) { + if (log.truncated) { items.splice(0, 0, new CommandQuickPickItem({ label: `$(sync) Show All Commits`, - description: `\u00a0 \u2014 \u00a0\u00a0 Currently only showing the first ${defaultMaxCount} commits`, + description: `\u00a0 \u2014 \u00a0\u00a0 Currently only showing the first ${log.maxCount} commits`, detail: `This may take a while` }, Commands.ShowQuickFileHistory, [uri, 0, goBackCommand])); + + const last = Iterables.last(log.commits.values()); + items.push(new CommandQuickPickItem({ + label: `$(ellipsis) Show More Commits`, + description: `\u00a0 \u2014 \u00a0\u00a0 Shows the next ${log.maxCount} commits` + }, Commands.ShowQuickFileHistory, [new GitUri(uri, last), log.maxCount, goBackCommand])); } // Only show the full repo option if we are the root @@ -28,12 +34,13 @@ export class FileHistoryQuickPick { detail: 'Shows the commit history of the repository' }, Commands.ShowQuickRepoHistory, [ + undefined, undefined, undefined, new CommandQuickPickItem({ label: `go back \u21A9`, description: `\u00a0 \u2014 \u00a0\u00a0 to history of \u00a0$(file-text) ${path.basename(uri.fsPath)}` - }, Commands.ShowQuickFileHistory, [uri, maxCount, undefined, log]) + }, Commands.ShowQuickFileHistory, [uri, log.maxCount, undefined, log]) ])); } @@ -41,10 +48,14 @@ export class FileHistoryQuickPick { items.splice(0, 0, goBackCommand); } + if (progressCancellation.token.isCancellationRequested) return undefined; + await Keyboard.instance.enterScope(['left', goBackCommand]); const commit = Iterables.first(log.commits.values()); + progressCancellation.cancel(); + const pick = await window.showQuickPick(items, { matchOnDescription: true, matchOnDetail: true, diff --git a/src/quickPicks/quickPicks.ts b/src/quickPicks/quickPicks.ts index 1dbf456..e686c82 100644 --- a/src/quickPicks/quickPicks.ts +++ b/src/quickPicks/quickPicks.ts @@ -1,12 +1,38 @@ 'use strict'; -import { commands, QuickPickItem, TextEditor, Uri, workspace } from 'vscode'; +import { CancellationTokenSource, commands, QuickPickItem, QuickPickOptions, TextEditor, Uri, window, workspace } from 'vscode'; import { Commands, openEditor } from '../commands'; import { IAdvancedConfig } from '../configuration'; +import { Logger } from '../logger'; export function getQuickPickIgnoreFocusOut() { return !workspace.getConfiguration('gitlens').get('advanced').quickPick.closeOnFocusOut; } +export function showQuickPickProgress(message: string): CancellationTokenSource { + const cancellation = new CancellationTokenSource(); + _showQuickPickProgress(message, cancellation); + return cancellation; +} + +async function _showQuickPickProgress(message: string, cancellation: CancellationTokenSource) { + Logger.log(`showQuickPickProgress`, `show`, message); + await window.showQuickPick(_getInfiniteCancellablePromise(cancellation), { + placeHolder: message, + ignoreFocusOut: getQuickPickIgnoreFocusOut() + } as QuickPickOptions, cancellation.token); + Logger.log(`showQuickPickProgress`, `cancel`, message); + cancellation.cancel(); +} + +function _getInfiniteCancellablePromise(cancellation: CancellationTokenSource) { + return new Promise((resolve, reject) => { + const disposable = cancellation.token.onCancellationRequested(() => { + disposable.dispose(); + reject(); + }); + }); +} + export class CommandQuickPickItem implements QuickPickItem { label: string; diff --git a/src/quickPicks/repoHistory.ts b/src/quickPicks/repoHistory.ts index f518d3b..241ba55 100644 --- a/src/quickPicks/repoHistory.ts +++ b/src/quickPicks/repoHistory.ts @@ -1,6 +1,6 @@ 'use strict'; import { Iterables } from '../system'; -import { QuickPickItem, QuickPickOptions, Uri, window } from 'vscode'; +import { CancellationTokenSource, QuickPickItem, QuickPickOptions, Uri, window } from 'vscode'; import { Commands, Keyboard } from '../commands'; import { IGitLog } from '../gitProvider'; import { CommitQuickPickItem } from './gitQuickPicks'; @@ -8,23 +8,33 @@ import { CommandQuickPickItem, getQuickPickIgnoreFocusOut } from './quickPicks'; export class RepoHistoryQuickPick { - static async show(log: IGitLog, uri: Uri, maxCount: number, defaultMaxCount: number, goBackCommand?: CommandQuickPickItem): Promise { + static async show(log: IGitLog, uri: Uri, sha: string, progressCancellation: CancellationTokenSource, goBackCommand?: CommandQuickPickItem): Promise { const items = Array.from(Iterables.map(log.commits.values(), c => new CommitQuickPickItem(c, ` \u2014 ${c.fileNames}`))) as (CommitQuickPickItem | CommandQuickPickItem)[]; - if (maxCount !== 0 && items.length >= defaultMaxCount) { + if (log.truncated) { items.splice(0, 0, new CommandQuickPickItem({ label: `$(sync) Show All Commits`, - description: `\u00a0 \u2014 \u00a0\u00a0 Currently only showing the first ${defaultMaxCount} commits`, + description: `\u00a0 \u2014 \u00a0\u00a0 Currently only showing the first ${log.maxCount} commits`, detail: `This may take a while` - }, Commands.ShowQuickRepoHistory, [uri, 0, goBackCommand])); + }, Commands.ShowQuickRepoHistory, [uri, undefined, 0, goBackCommand])); + + const last = Iterables.last(log.commits.values()); + items.push(new CommandQuickPickItem({ + label: `$(ellipsis) Show More Commits`, + description: `\u00a0 \u2014 \u00a0\u00a0 Shows the next ${log.maxCount} commits` + }, Commands.ShowQuickRepoHistory, [uri, last.sha, log.maxCount, goBackCommand])); } if (goBackCommand) { items.splice(0, 0, goBackCommand); } + if (progressCancellation.token.isCancellationRequested) return undefined; + await Keyboard.instance.enterScope(['left', goBackCommand]); + progressCancellation.cancel(); + const pick = await window.showQuickPick(items, { matchOnDescription: true, matchOnDetail: true,