mirror of
https://github.com/ckaczor/vscode-gitlens.git
synced 2026-02-17 02:51:47 -05:00
Adds .gitignore checks to reduce blame calls
Caches failed blames to reduce blame calls Only clear failed blames from cache on change/save Add better error messages and handling
This commit is contained in:
@@ -83,6 +83,7 @@
|
|||||||
"*"
|
"*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ignore": "^3.1.5",
|
||||||
"lodash": "^4.15.0",
|
"lodash": "^4.15.0",
|
||||||
"moment": "^2.15.0",
|
"moment": "^2.15.0",
|
||||||
"spawn-rx": "^2.0.1",
|
"spawn-rx": "^2.0.1",
|
||||||
|
|||||||
13
src/git.ts
13
src/git.ts
@@ -5,11 +5,18 @@ import * as tmp from 'tmp';
|
|||||||
import {spawnPromise} from 'spawn-rx';
|
import {spawnPromise} from 'spawn-rx';
|
||||||
|
|
||||||
function gitCommand(cwd: string, ...args) {
|
function gitCommand(cwd: string, ...args) {
|
||||||
console.log('[GitLens]', 'git', ...args);
|
|
||||||
return spawnPromise('git', args, { cwd: cwd })
|
return spawnPromise('git', args, { cwd: cwd })
|
||||||
// .then(s => { console.log('[GitLens]', s); return s; })
|
.then(s => {
|
||||||
|
console.log('[GitLens]', 'git', ...args);
|
||||||
|
return s;
|
||||||
|
})
|
||||||
.catch(ex => {
|
.catch(ex => {
|
||||||
console.error('[GitLens]', 'git', ...args, 'Failed:', ex);
|
const msg = ex && ex.toString();
|
||||||
|
if (msg && (msg.includes('is outside repository') || msg.includes('no such path'))) {
|
||||||
|
console.warn('[GitLens]', 'git', ...args, msg && msg.replace(/\r?\n|\r/g, ' '));
|
||||||
|
} else {
|
||||||
|
console.error('[GitLens]', 'git', ...args, msg && msg.replace(/\r?\n|\r/g, ' '));
|
||||||
|
}
|
||||||
throw ex;
|
throw ex;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export default class GitBlameCodeLensProvider implements CodeLensProvider {
|
|||||||
const sha = data.sha;
|
const sha = data.sha;
|
||||||
|
|
||||||
return this.git.getBlameForFile(fileName).then(blame => {
|
return this.git.getBlameForFile(fileName).then(blame => {
|
||||||
|
const lenses: CodeLens[] = [];
|
||||||
|
if (!blame) return lenses;
|
||||||
|
|
||||||
const commits = Array.from(blame.commits.values());
|
const commits = Array.from(blame.commits.values());
|
||||||
let index = commits.findIndex(c => c.sha === sha) + 1;
|
let index = commits.findIndex(c => c.sha === sha) + 1;
|
||||||
|
|
||||||
@@ -36,8 +39,6 @@ export default class GitBlameCodeLensProvider implements CodeLensProvider {
|
|||||||
previousCommit = commits[index];
|
previousCommit = commits[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
const lenses: CodeLens[] = [];
|
|
||||||
|
|
||||||
// Add codelens to each "group" of blame lines
|
// Add codelens to each "group" of blame lines
|
||||||
const lines = blame.lines.filter(l => l.sha === sha && l.originalLine >= data.range.start.line && l.originalLine <= data.range.end.line);
|
const lines = blame.lines.filter(l => l.sha === sha && l.originalLine >= data.range.start.line && l.originalLine <= data.range.end.line);
|
||||||
let lastLine = lines[0].originalLine;
|
let lastLine = lines[0].originalLine;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export default class GitBlameContentProvider implements TextDocumentContentProvi
|
|||||||
|
|
||||||
clearInterval(handle);
|
clearInterval(handle);
|
||||||
this.git.getBlameForShaRange(data.fileName, data.sha, data.range).then(blame => {
|
this.git.getBlameForShaRange(data.fileName, data.sha, data.range).then(blame => {
|
||||||
if (!blame.lines.length) return;
|
if (!blame || !blame.lines.length) return;
|
||||||
|
|
||||||
editor.setDecorations(this._blameDecoration, blame.lines.map(l => {
|
editor.setDecorations(this._blameDecoration, blame.lines.map(l => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class GitBlameEditorController extends Disposable {
|
|||||||
|
|
||||||
applyBlame(sha?: string) {
|
applyBlame(sha?: string) {
|
||||||
return this._blame.then(blame => {
|
return this._blame.then(blame => {
|
||||||
if (!blame.lines.length) return;
|
if (!blame || !blame.lines.length) return;
|
||||||
|
|
||||||
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace off
|
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace off
|
||||||
this._toggleWhitespace = workspace.getConfiguration('editor').get('renderWhitespace') as boolean;
|
this._toggleWhitespace = workspace.getConfiguration('editor').get('renderWhitespace') as boolean;
|
||||||
@@ -212,7 +212,7 @@ class GitBlameEditorController extends Disposable {
|
|||||||
|
|
||||||
applyHighlight(sha: string) {
|
applyHighlight(sha: string) {
|
||||||
return this._blame.then(blame => {
|
return this._blame.then(blame => {
|
||||||
if (!blame.lines.length) return;
|
if (!blame || !blame.lines.length) return;
|
||||||
|
|
||||||
const highlightDecorationRanges = blame.lines
|
const highlightDecorationRanges = blame.lines
|
||||||
.filter(l => l.sha === sha)
|
.filter(l => l.sha === sha)
|
||||||
|
|||||||
@@ -6,35 +6,70 @@ import Git from './git';
|
|||||||
import {basename, dirname, extname, join} from 'path';
|
import {basename, dirname, extname, join} from 'path';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
import {exists, readFile} from 'fs'
|
||||||
|
import * as ignore from 'ignore';
|
||||||
|
|
||||||
const commitMessageMatcher = /^([\^0-9a-fA-F]{7})\s(.*)$/gm;
|
const commitMessageMatcher = /^([\^0-9a-fA-F]{7})\s(.*)$/gm;
|
||||||
const blamePorcelainMatcher = /^([\^0-9a-fA-F]{40})\s([0-9]+)\s([0-9]+)(?:\s([0-9]+))?$\n(?:^author\s(.*)$\n^author-mail\s(.*)$\n^author-time\s(.*)$\n^author-tz\s(.*)$\n^committer\s(.*)$\n^committer-mail\s(.*)$\n^committer-time\s(.*)$\n^committer-tz\s(.*)$\n^summary\s(.*)$\n(?:^previous\s(.*)?\s(.*)$\n)?^filename\s(.*)$\n)?^(.*)$/gm;
|
const blamePorcelainMatcher = /^([\^0-9a-fA-F]{40})\s([0-9]+)\s([0-9]+)(?:\s([0-9]+))?$\n(?:^author\s(.*)$\n^author-mail\s(.*)$\n^author-time\s(.*)$\n^author-tz\s(.*)$\n^committer\s(.*)$\n^committer-mail\s(.*)$\n^committer-time\s(.*)$\n^committer-tz\s(.*)$\n^summary\s(.*)$\n(?:^previous\s(.*)?\s(.*)$\n)?^filename\s(.*)$\n)?^(.*)$/gm;
|
||||||
|
|
||||||
|
interface IBlameCacheEntry {
|
||||||
|
//date: Date;
|
||||||
|
blame: Promise<IGitBlame>;
|
||||||
|
errorMessage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RemoveCacheReason {
|
||||||
|
DocumentClosed,
|
||||||
|
DocumentSaved,
|
||||||
|
DocumentChanged
|
||||||
|
}
|
||||||
|
|
||||||
export default class GitProvider extends Disposable {
|
export default class GitProvider extends Disposable {
|
||||||
public repoPath: string;
|
public repoPath: string;
|
||||||
|
|
||||||
private _blames: Map<string, Promise<IGitBlame>>;
|
private _blames: Map<string, IBlameCacheEntry>;
|
||||||
private _disposable: Disposable;
|
private _disposable: Disposable;
|
||||||
private _codeLensProviderSubscription: Disposable;
|
private _codeLensProviderSubscription: Disposable;
|
||||||
|
private _gitignore: Promise<ignore.Ignore>;
|
||||||
|
|
||||||
// TODO: Needs to be a Map so it can debounce per file
|
// TODO: Needs to be a Map so it can debounce per file
|
||||||
private _clearCacheFn: ((string, boolean) => void) & _.Cancelable;
|
private _removeCachedBlameFn: ((string, boolean) => void) & _.Cancelable;
|
||||||
|
|
||||||
|
static BlameEmptyPromise = Promise.resolve(<IGitBlame>null);
|
||||||
|
|
||||||
constructor(private context: ExtensionContext) {
|
constructor(private context: ExtensionContext) {
|
||||||
super(() => this.dispose());
|
super(() => this.dispose());
|
||||||
|
|
||||||
this.repoPath = context.workspaceState.get(WorkspaceState.RepoPath) as string;
|
this.repoPath = context.workspaceState.get(WorkspaceState.RepoPath) as string;
|
||||||
|
|
||||||
|
this._gitignore = new Promise<ignore.Ignore>((resolve, reject) => {
|
||||||
|
const gitignorePath = join(this.repoPath, '.gitignore');
|
||||||
|
exists(gitignorePath, e => {
|
||||||
|
if (e) {
|
||||||
|
readFile(gitignorePath, 'utf8', (err, data) => {
|
||||||
|
if (!err) {
|
||||||
|
resolve(ignore().add(data));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: Cache needs to be cleared on file changes -- createFileSystemWatcher or timeout?
|
// TODO: Cache needs to be cleared on file changes -- createFileSystemWatcher or timeout?
|
||||||
this._blames = new Map();
|
this._blames = new Map();
|
||||||
this._registerCodeLensProvider();
|
this._registerCodeLensProvider();
|
||||||
this._clearCacheFn = _.debounce(this._clearBlame.bind(this), 2500);
|
this._removeCachedBlameFn = _.debounce(this._removeCachedBlame.bind(this), 2500);
|
||||||
|
|
||||||
const subscriptions: Disposable[] = [];
|
const subscriptions: Disposable[] = [];
|
||||||
|
|
||||||
subscriptions.push(workspace.onDidCloseTextDocument(d => this._clearBlame(d.fileName)));
|
// TODO: Maybe stop clearing on close and instead limit to a certain number of recent blames
|
||||||
subscriptions.push(workspace.onDidSaveTextDocument(d => this._clearCacheFn(d.fileName, true)));
|
subscriptions.push(workspace.onDidCloseTextDocument(d => this._removeCachedBlame(d.fileName, RemoveCacheReason.DocumentClosed)));
|
||||||
subscriptions.push(workspace.onDidChangeTextDocument(e => this._clearCacheFn(e.document.fileName, false)));
|
subscriptions.push(workspace.onDidSaveTextDocument(d => this._removeCachedBlameFn(d.fileName, RemoveCacheReason.DocumentSaved)));
|
||||||
|
subscriptions.push(workspace.onDidChangeTextDocument(e => this._removeCachedBlameFn(e.document.fileName, RemoveCacheReason.DocumentChanged)));
|
||||||
|
|
||||||
this._disposable = Disposable.from(...subscriptions);
|
this._disposable = Disposable.from(...subscriptions);
|
||||||
}
|
}
|
||||||
@@ -52,17 +87,27 @@ export default class GitProvider extends Disposable {
|
|||||||
this._codeLensProviderSubscription = languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(this.context, this));
|
this._codeLensProviderSubscription = languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(this.context, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearBlame(fileName: string, reset?: boolean) {
|
private _getBlameCacheKey(fileName: string) {
|
||||||
fileName = Git.normalizePath(fileName, this.repoPath);
|
return fileName.toLowerCase();
|
||||||
reset = !!reset;
|
|
||||||
|
|
||||||
if (this._blames.delete(fileName.toLowerCase())) {
|
|
||||||
console.log('[GitLens]', `Clear blame cache: fileName=${fileName}, reset=${reset})`);
|
|
||||||
|
|
||||||
if (reset) {
|
|
||||||
// TODO: Killing the code lens provider is too drastic -- makes the editor jump around, need to figure out how to trigger a refresh
|
|
||||||
//this._registerCodeLensProvider();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _removeCachedBlame(fileName: string, reason: RemoveCacheReason) {
|
||||||
|
fileName = Git.normalizePath(fileName, this.repoPath);
|
||||||
|
|
||||||
|
const cacheKey = this._getBlameCacheKey(fileName);
|
||||||
|
if (reason === RemoveCacheReason.DocumentClosed) {
|
||||||
|
// Don't remove broken blame on close (since otherwise we'll have to run the broken blame again)
|
||||||
|
const entry = this._blames.get(cacheKey);
|
||||||
|
if (entry && entry.errorMessage) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._blames.delete(cacheKey)) {
|
||||||
|
console.log('[GitLens]', `Clear blame cache: fileName=${fileName}, reason=${RemoveCacheReason[reason]})`);
|
||||||
|
|
||||||
|
// if (reason === RemoveCacheReason.DocumentSaved) {
|
||||||
|
// // TODO: Killing the code lens provider is too drastic -- makes the editor jump around, need to figure out how to trigger a refresh
|
||||||
|
// this._registerCodeLensProvider();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,11 +118,20 @@ export default class GitProvider extends Disposable {
|
|||||||
getBlameForFile(fileName: string) {
|
getBlameForFile(fileName: string) {
|
||||||
fileName = Git.normalizePath(fileName, this.repoPath);
|
fileName = Git.normalizePath(fileName, this.repoPath);
|
||||||
|
|
||||||
let blame = this._blames.get(fileName.toLowerCase());
|
const cacheKey = this._getBlameCacheKey(fileName);
|
||||||
if (blame !== undefined) return blame;
|
let entry = this._blames.get(cacheKey);
|
||||||
|
if (entry !== undefined) return entry.blame;
|
||||||
|
|
||||||
|
return this._gitignore.then(ignore => {
|
||||||
|
let blame: Promise<IGitBlame>;
|
||||||
|
if (ignore && !ignore.filter([fileName]).length) {
|
||||||
|
console.log('[GitLens]', `Skipping blame; ${fileName} is gitignored`);
|
||||||
|
blame = GitProvider.BlameEmptyPromise;
|
||||||
|
} else {
|
||||||
blame = Git.blamePorcelain(fileName, this.repoPath)
|
blame = Git.blamePorcelain(fileName, this.repoPath)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
const authors: Map<string, IGitAuthor> = new Map();
|
const authors: Map<string, IGitAuthor> = new Map();
|
||||||
const commits: Map<string, IGitCommit> = new Map();
|
const commits: Map<string, IGitCommit> = new Map();
|
||||||
const lines: Array<IGitCommitLine> = [];
|
const lines: Array<IGitCommitLine> = [];
|
||||||
@@ -143,14 +197,41 @@ export default class GitProvider extends Disposable {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
this._blames.set(fileName.toLowerCase(), blame);
|
// Trap and cache expected blame errors
|
||||||
|
blame.catch(ex => {
|
||||||
|
const msg = ex && ex.toString();
|
||||||
|
if (msg && (msg.includes('is outside repository') || msg.includes('no such path'))) {
|
||||||
|
this._blames.set(cacheKey, <IBlameCacheEntry>{
|
||||||
|
//date: new Date(),
|
||||||
|
blame: GitProvider.BlameEmptyPromise,
|
||||||
|
errorMessage: msg
|
||||||
|
});
|
||||||
|
return GitProvider.BlameEmptyPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const brokenBlame = this._blames.get(cacheKey);
|
||||||
|
if (brokenBlame) {
|
||||||
|
brokenBlame.errorMessage = msg;
|
||||||
|
this._blames.set(cacheKey, brokenBlame);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ex;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._blames.set(cacheKey, <IBlameCacheEntry> {
|
||||||
|
//date: new Date(),
|
||||||
|
blame: blame
|
||||||
|
});
|
||||||
|
|
||||||
return blame;
|
return blame;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlameForLine(fileName: string, line: number): Promise<IGitBlameLine> {
|
getBlameForLine(fileName: string, line: number): Promise<IGitBlameLine> {
|
||||||
return this.getBlameForFile(fileName).then(blame => {
|
return this.getBlameForFile(fileName).then(blame => {
|
||||||
const blameLine = blame.lines[line];
|
const blameLine = blame && blame.lines[line];
|
||||||
if (!blameLine) return undefined;
|
if (!blameLine) return null;
|
||||||
|
|
||||||
const commit = blame.commits.get(blameLine.sha);
|
const commit = blame.commits.get(blameLine.sha);
|
||||||
return {
|
return {
|
||||||
@@ -163,6 +244,8 @@ export default class GitProvider extends Disposable {
|
|||||||
|
|
||||||
getBlameForRange(fileName: string, range: Range): Promise<IGitBlameLines> {
|
getBlameForRange(fileName: string, range: Range): Promise<IGitBlameLines> {
|
||||||
return this.getBlameForFile(fileName).then(blame => {
|
return this.getBlameForFile(fileName).then(blame => {
|
||||||
|
if (!blame) return null;
|
||||||
|
|
||||||
if (!blame.lines.length) return Object.assign({ allLines: blame.lines }, blame);
|
if (!blame.lines.length) return Object.assign({ allLines: blame.lines }, blame);
|
||||||
|
|
||||||
if (range.start.line === 0 && range.end.line === blame.lines.length - 1) {
|
if (range.start.line === 0 && range.end.line === blame.lines.length - 1) {
|
||||||
@@ -209,6 +292,8 @@ export default class GitProvider extends Disposable {
|
|||||||
|
|
||||||
getBlameForShaRange(fileName: string, sha: string, range: Range): Promise<IGitBlameCommitLines> {
|
getBlameForShaRange(fileName: string, sha: string, range: Range): Promise<IGitBlameCommitLines> {
|
||||||
return this.getBlameForFile(fileName).then(blame => {
|
return this.getBlameForFile(fileName).then(blame => {
|
||||||
|
if (!blame) return null;
|
||||||
|
|
||||||
const lines = blame.lines.slice(range.start.line, range.end.line + 1).filter(l => l.sha === sha);
|
const lines = blame.lines.slice(range.start.line, range.end.line + 1).filter(l => l.sha === sha);
|
||||||
let commit = blame.commits.get(sha);
|
let commit = blame.commits.get(sha);
|
||||||
commit = new GitCommit(this.repoPath, commit.sha, commit.fileName, commit.author, commit.date, commit.message, lines);
|
commit = new GitCommit(this.repoPath, commit.sha, commit.fileName, commit.author, commit.date, commit.message, lines);
|
||||||
@@ -222,6 +307,8 @@ export default class GitProvider extends Disposable {
|
|||||||
|
|
||||||
getBlameLocations(fileName: string, range: Range) {
|
getBlameLocations(fileName: string, range: Range) {
|
||||||
return this.getBlameForRange(fileName, range).then(blame => {
|
return this.getBlameForRange(fileName, range).then(blame => {
|
||||||
|
if (!blame) return null;
|
||||||
|
|
||||||
const commitCount = blame.commits.size;
|
const commitCount = blame.commits.size;
|
||||||
|
|
||||||
const locations: Array<Location> = [];
|
const locations: Array<Location> = [];
|
||||||
@@ -240,6 +327,8 @@ export default class GitProvider extends Disposable {
|
|||||||
|
|
||||||
// getHistoryLocations(fileName: string, range: Range) {
|
// getHistoryLocations(fileName: string, range: Range) {
|
||||||
// return this.getBlameForRange(fileName, range).then(blame => {
|
// return this.getBlameForRange(fileName, range).then(blame => {
|
||||||
|
// if (!blame) return null;
|
||||||
|
|
||||||
// const commitCount = blame.commits.size;
|
// const commitCount = blame.commits.size;
|
||||||
|
|
||||||
// const locations: Array<Location> = [];
|
// const locations: Array<Location> = [];
|
||||||
|
|||||||
10
typings/ignore.d.ts
vendored
Normal file
10
typings/ignore.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
declare module "ignore" {
|
||||||
|
namespace ignore {
|
||||||
|
interface Ignore {
|
||||||
|
add(patterns: string | Array<string> | Ignore): Ignore;
|
||||||
|
filter(paths: Array<string>): Array<string>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function ignore(): ignore.Ignore;
|
||||||
|
export = ignore;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user