Skip to content

Commit 1576c08

Browse files
author
Eric Amodio
committed
Reworked git access
Cleaned up the blame hightlights (wip)
1 parent 03d4cc8 commit 1576c08

File tree

8 files changed

+131
-149
lines changed

8 files changed

+131
-149
lines changed
24.2 KB
Loading

images/blame-light.png

24.1 KB
Loading

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@
3131
"postinstall": "node ./node_modules/vscode/bin/install && tsc"
3232
},
3333
"dependencies": {
34-
"tmp": "^0.0.28"
34+
"tmp": "^0.0.28",
35+
"spawn-rx": "^2.0.1"
3536
},
3637
"devDependencies": {
3738
"typescript": "^1.8.10",
38-
"vscode": "^0.11.15"
39+
"vscode": "^0.11.17"
3940
}
4041
}

src/codeLensProvider.ts

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import {CancellationToken, CodeLens, CodeLensProvider, commands, Location, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode';
33
import {Commands, VsCodeCommands} from './constants';
44
import {IGitBlameLine, gitBlame} from './git';
5-
import {toGitBlameUri} from './contentProvider';
5+
import {toGitBlameUri} from './gitBlameUri';
66
import * as moment from 'moment';
77

88
export class GitBlameCodeLens extends CodeLens {
@@ -34,7 +34,7 @@ export default class GitCodeLensProvider implements CodeLensProvider {
3434

3535
provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable<CodeLens[]> {
3636
// TODO: Should I wait here?
37-
let blame = gitBlame(document.fileName);
37+
const blame = gitBlame(document.fileName);
3838

3939
return (commands.executeCommand(VsCodeCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise<SymbolInformation[]>).then(symbols => {
4040
let lenses: CodeLens[] = [];
@@ -44,6 +44,7 @@ export default class GitCodeLensProvider implements CodeLensProvider {
4444
if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) {
4545
const docRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
4646
lenses.push(new GitBlameCodeLens(blame, this.repoPath, document.fileName, docRange, new Range(0, 0, 0, docRange.start.character)));
47+
lenses.push(new GitHistoryCodeLens(this.repoPath, document.fileName, docRange.with(new Position(docRange.start.line, docRange.start.character + 1))));
4748
}
4849
return lenses;
4950
});
@@ -66,67 +67,66 @@ export default class GitCodeLensProvider implements CodeLensProvider {
6667
return;
6768
}
6869

69-
var line = document.lineAt(symbol.location.range.start);
70+
const line = document.lineAt(symbol.location.range.start);
7071
lenses.push(new GitBlameCodeLens(blame, this.repoPath, document.fileName, symbol.location.range, line.range.with(new Position(line.range.start.line, line.firstNonWhitespaceCharacterIndex))));
7172
lenses.push(new GitHistoryCodeLens(this.repoPath, document.fileName, line.range.with(new Position(line.range.start.line, line.firstNonWhitespaceCharacterIndex + 1))));
7273
}
7374

7475
resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable<CodeLens> {
75-
if (lens instanceof GitBlameCodeLens) {
76-
return lens.getBlameLines().then(lines => {
77-
if (!lines.length) {
78-
console.error('No blame lines found', lens);
79-
throw new Error('No blame lines found');
80-
}
81-
82-
let recentLine = lines[0];
83-
84-
let locations: Location[] = [];
85-
if (lines.length > 1) {
86-
let sorted = lines.sort((a, b) => b.date.getTime() - a.date.getTime());
87-
recentLine = sorted[0];
88-
89-
console.log(lens.fileName, 'Blame lines:', sorted);
90-
91-
let map: Map<string, IGitBlameLine[]> = new Map();
92-
sorted.forEach(l => {
93-
let item = map.get(l.sha);
94-
if (item) {
95-
item.push(l);
96-
} else {
97-
map.set(l.sha, [l]);
98-
}
99-
});
100-
101-
Array.from(map.values()).forEach((lines, i) => {
102-
const uri = GitBlameCodeLens.toUri(lens, i + 1, lines[0], lines);
103-
lines.forEach(l => {
104-
locations.push(new Location(uri, new Position(l.originalLine, 0)));
105-
});
106-
});
107-
108-
//locations = Array.from(map.values()).map((l, i) => new Location(GitBlameCodeLens.toUri(lens, i, l[0], l), new Position(l[0].originalLine, 0)));//lens.range.start))
109-
} else {
110-
locations = [new Location(GitBlameCodeLens.toUri(lens, 1, recentLine, lines), lens.range.start)];
111-
}
112-
113-
lens.command = {
114-
title: `${recentLine.author}, ${moment(recentLine.date).fromNow()}`,
115-
command: Commands.ShowBlameHistory,
116-
arguments: [Uri.file(lens.fileName), lens.range.start, locations]
117-
};
118-
return lens;
119-
}).catch(ex => Promise.reject(ex)); // TODO: Figure out a better way to stop the codelens from appearing
120-
}
76+
if (lens instanceof GitBlameCodeLens) return this._resolveGitBlameCodeLens(lens, token);
77+
if (lens instanceof GitHistoryCodeLens) return this._resolveGitHistoryCodeLens(lens, token);
78+
}
79+
80+
_resolveGitBlameCodeLens(lens: GitBlameCodeLens, token: CancellationToken): Thenable<CodeLens> {
81+
return lens.getBlameLines().then(lines => {
82+
if (!lines.length) {
83+
console.error('No blame lines found', lens);
84+
throw new Error('No blame lines found');
85+
}
86+
87+
let recentLine = lines[0];
88+
89+
let locations: Location[] = [];
90+
if (lines.length > 1) {
91+
let sorted = lines.sort((a, b) => b.date.getTime() - a.date.getTime());
92+
recentLine = sorted[0];
93+
94+
console.log(lens.fileName, 'Blame lines:', sorted);
95+
96+
let map: Map<string, IGitBlameLine[]> = new Map();
97+
sorted.forEach(l => {
98+
let item = map.get(l.sha);
99+
if (item) {
100+
item.push(l);
101+
} else {
102+
map.set(l.sha, [l]);
103+
}
104+
});
105+
106+
Array.from(map.values()).forEach((lines, i) => {
107+
const uri = GitBlameCodeLens.toUri(lens, i + 1, lines[0], lines);
108+
lines.forEach(l => locations.push(new Location(uri, new Position(l.originalLine, 0))));
109+
});
110+
} else {
111+
locations = [new Location(GitBlameCodeLens.toUri(lens, 1, recentLine, lines), lens.range.start)];
112+
}
121113

122-
// TODO: Play with this more -- get this to open the correct diff to the right place
123-
if (lens instanceof GitHistoryCodeLens) {
124114
lens.command = {
125-
title: `View Diff`,
126-
command: 'git.viewFileHistory', // viewLineHistory
127-
arguments: [Uri.file(lens.fileName)]
115+
title: `${recentLine.author}, ${moment(recentLine.date).fromNow()}`,
116+
command: Commands.ShowBlameHistory,
117+
arguments: [Uri.file(lens.fileName), lens.range.start, locations]
128118
};
129-
return Promise.resolve(lens);
130-
}
119+
return lens;
120+
}).catch(ex => Promise.reject(ex)); // TODO: Figure out a better way to stop the codelens from appearing
121+
}
122+
123+
_resolveGitHistoryCodeLens(lens: GitHistoryCodeLens, token: CancellationToken): Thenable<CodeLens> {
124+
// TODO: Play with this more -- get this to open the correct diff to the right place
125+
lens.command = {
126+
title: `View History`,
127+
command: 'git.viewFileHistory', // viewLineHistory
128+
arguments: [Uri.file(lens.fileName)]
129+
};
130+
return Promise.resolve(lens);
131131
}
132132
}

src/contentProvider.ts

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,38 @@
11
'use strict';
22
import {Disposable, EventEmitter, ExtensionContext, OverviewRulerLane, Range, TextEditor, TextEditorDecorationType, TextDocumentContentProvider, Uri, window, workspace} from 'vscode';
33
import {DocumentSchemes} from './constants';
4-
import {gitGetVersionFile, gitGetVersionText, IGitBlameLine} from './git';
5-
import {basename, dirname, extname, join} from 'path';
4+
import {gitGetVersionText} from './git';
5+
import {fromGitBlameUri, IGitBlameUriData} from './gitBlameUri';
66
import * as moment from 'moment';
77

88
export default class GitBlameContentProvider implements TextDocumentContentProvider {
99
static scheme = DocumentSchemes.GitBlame;
1010

1111
private _blameDecoration: TextEditorDecorationType;
1212
private _onDidChange = new EventEmitter<Uri>();
13-
private _subscriptions: Disposable;
13+
// private _subscriptions: Disposable;
1414
// private _dataMap: Map<string, IGitBlameUriData>;
1515

1616
constructor(context: ExtensionContext) {
17-
// TODO: Light & Dark
1817
this._blameDecoration = window.createTextEditorDecorationType({
19-
backgroundColor: 'rgba(254, 220, 95, 0.15)',
20-
gutterIconPath: context.asAbsolutePath('blame.png'),
21-
overviewRulerColor: 'rgba(254, 220, 95, 0.60)',
18+
dark: {
19+
backgroundColor: 'rgba(255, 255, 255, 0.15)',
20+
gutterIconPath: context.asAbsolutePath('images/blame-dark.png'),
21+
overviewRulerColor: 'rgba(255, 255, 255, 0.75)',
22+
},
23+
light: {
24+
backgroundColor: 'rgba(0, 0, 0, 0.15)',
25+
gutterIconPath: context.asAbsolutePath('images/blame-light.png'),
26+
overviewRulerColor: 'rgba(0, 0, 0, 0.75)',
27+
},
28+
gutterIconSize: 'contain',
2229
overviewRulerLane: OverviewRulerLane.Right,
2330
isWholeLine: true
2431
});
2532

2633
// this._dataMap = new Map();
2734
// this._subscriptions = Disposable.from(
35+
// window.onDidChangeActiveTextEditor(e => e ? console.log(e.document.uri) : console.log('active missing')),
2836
// workspace.onDidOpenTextDocument(d => {
2937
// let data = this._dataMap.get(d.uri.toString());
3038
// if (!data) return;
@@ -40,7 +48,7 @@ export default class GitBlameContentProvider implements TextDocumentContentProvi
4048

4149
dispose() {
4250
this._onDidChange.dispose();
43-
this._subscriptions && this._subscriptions.dispose();
51+
// this._subscriptions && this._subscriptions.dispose();
4452
}
4553

4654
get onDidChange() {
@@ -57,7 +65,6 @@ export default class GitBlameContentProvider implements TextDocumentContentProvi
5765

5866
//const editor = this._findEditor(Uri.file(join(data.repoPath, data.file)));
5967

60-
//console.log('provideTextDocumentContent', uri, data);
6168
return gitGetVersionText(data.repoPath, data.sha, data.file).then(text => {
6269
this.update(uri);
6370

@@ -81,6 +88,7 @@ export default class GitBlameContentProvider implements TextDocumentContentProvi
8188

8289
private _findEditor(uri: Uri): TextEditor {
8390
let uriString = uri.toString();
91+
// TODO: This is a big hack :)
8492
const matcher = (e: any) => (e._documentData && e._documentData._uri && e._documentData._uri.toString()) === uriString;
8593
if (matcher(window.activeTextEditor)) {
8694
return window.activeTextEditor;
@@ -89,14 +97,15 @@ export default class GitBlameContentProvider implements TextDocumentContentProvi
8997
}
9098

9199
private _tryAddBlameDecorations(uri: Uri, data: IGitBlameUriData) {
100+
// Needs to be on a timer for some reason because we won't find the editor otherwise -- is there an event?
92101
let handle = setInterval(() => {
93102
let editor = this._findEditor(uri);
94103
if (editor) {
95104
clearInterval(handle);
96105
editor.setDecorations(this._blameDecoration, data.lines.map(l => {
97106
return {
98107
range: editor.document.validateRange(new Range(l.originalLine, 0, l.originalLine, 1000000)),
99-
hoverMessage: `${moment(l.date).fromNow()}\n${l.author}\n${l.sha}`
108+
hoverMessage: `${moment(l.date).format('MMMM Do, YYYY hh:MMa')}\n${l.author}\n${l.sha}`
100109
};
101110
}));
102111
}
@@ -106,23 +115,4 @@ export default class GitBlameContentProvider implements TextDocumentContentProvi
106115
// private _addBlameDecorations(editor: TextEditor, data: IGitBlameUriData) {
107116
// editor.setDecorations(this._blameDecoration, data.lines.map(l => editor.document.validateRange(new Range(l.line, 0, l.line, 1000000))));
108117
// }
109-
}
110-
111-
export interface IGitBlameUriData extends IGitBlameLine {
112-
repoPath: string,
113-
range: Range,
114-
index: number,
115-
lines: IGitBlameLine[]
116-
}
117-
118-
export function toGitBlameUri(data: IGitBlameUriData) {
119-
let ext = extname(data.file);
120-
let path = `${dirname(data.file)}/${data.sha}: ${basename(data.file, ext)}${ext}`;
121-
return Uri.parse(`${DocumentSchemes.GitBlame}:${data.index}. ${moment(data.date).format('YYYY-MM-DD hh:MMa')} ${path}?${JSON.stringify(data)}`);
122-
}
123-
124-
export function fromGitBlameUri(uri: Uri): IGitBlameUriData {
125-
let data = JSON.parse(uri.query);
126-
data.range = new Range(data.range[0].line, data.range[0].character, data.range[1].line, data.range[1].character);
127-
return data;
128118
}

src/git.ts

Lines changed: 13 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
'use strict';
2-
import {spawn} from 'child_process';
32
import {basename, dirname, extname} from 'path';
43
import * as fs from 'fs';
54
import * as tmp from 'tmp';
5+
import {spawnPromise} from 'spawn-rx';
66

77
export declare interface IGitBlameLine {
88
sha: string;
@@ -14,28 +14,14 @@ export declare interface IGitBlameLine {
1414
code: string;
1515
}
1616

17-
export function gitRepoPath(cwd): Promise<string> {
18-
let data: Array<string> = [];
19-
const capture = input => data.push(input.toString().replace(/\r?\n|\r/g, ''));
20-
const output = () => data[0];
21-
22-
return gitCommand(cwd, capture, output, 'rev-parse', '--show-toplevel');
23-
24-
// return new Promise<string>((resolve, reject) => {
25-
// gitCommand(cwd, capture, output, 'rev-parse', '--show-toplevel')
26-
// .then(result => resolve(result[0]))
27-
// .catch(reason => reject(reason));
28-
// });
17+
export function gitRepoPath(cwd) {
18+
return gitCommand(cwd, 'rev-parse', '--show-toplevel').then(data => data.replace(/\r?\n|\r/g, ''));
2919
}
3020

31-
//const blameMatcher = /^(.*)\t\((.*)\t(.*)\t(.*?)\)(.*)$/gm;
32-
//const blameMatcher = /^([0-9a-fA-F]{8})\s([\S]*)\s([0-9\S]+)\s\((.*?)\s([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\s[-|+][0-9]{4})\s([0-9]+)\)(.*)$/gm;
3321
const blameMatcher = /^([0-9a-fA-F]{8})\s([\S]*)\s+([0-9\S]+)\s\((.*)\s([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\s[-|+][0-9]{4})\s+([0-9]+)\)(.*)$/gm;
3422

35-
export function gitBlame(fileName: string): Promise<IGitBlameLine[]> {
36-
let data: string = '';
37-
const capture = input => data += input.toString();
38-
const output = () => {
23+
export function gitBlame(fileName: string) {
24+
return gitCommand(dirname(fileName), 'blame', '-fnw', '--', fileName).then(data => {
3925
let lines: Array<IGitBlameLine> = [];
4026
let m: Array<string>;
4127
while ((m = blameMatcher.exec(data)) != null) {
@@ -50,18 +36,12 @@ export function gitBlame(fileName: string): Promise<IGitBlameLine[]> {
5036
});
5137
}
5238
return lines;
53-
};
54-
55-
return gitCommand(dirname(fileName), capture, output, 'blame', '-fnw', '--', fileName);
39+
});
5640
}
5741

58-
export function gitGetVersionFile(repoPath: string, sha: string, source: string): Promise<any> {
59-
let data: Array<any> = [];
60-
const capture = input => data.push(input);
61-
const output = () => data;
62-
42+
export function gitGetVersionFile(repoPath: string, sha: string, source: string): Promise<string> {
6343
return new Promise<string>((resolve, reject) => {
64-
(gitCommand(repoPath, capture, output, 'show', `${sha}:${source.replace(/\\/g, '/')}`) as Promise<Array<Buffer>>).then(o => {
44+
gitCommand(repoPath, 'show', `${sha}:${source.replace(/\\/g, '/')}`).then(data => {
6545
let ext = extname(source);
6646
tmp.file({ prefix: `${basename(source, ext)}-${sha}_`, postfix: ext }, (err, destination, fd, cleanupCallback) => {
6747
if (err) {
@@ -72,7 +52,7 @@ export function gitGetVersionFile(repoPath: string, sha: string, source: string)
7252
console.log("File: ", destination);
7353
console.log("Filedescriptor: ", fd);
7454

75-
fs.appendFile(destination, o.join(), err => {
55+
fs.appendFile(destination, data, err => {
7656
if (err) {
7757
reject(err);
7858
return;
@@ -84,39 +64,10 @@ export function gitGetVersionFile(repoPath: string, sha: string, source: string)
8464
});
8565
}
8666

87-
export function gitGetVersionText(repoPath: string, sha: string, source: string): Promise<string> {
88-
let data: Array<string> = [];
89-
const capture = input => data.push(input.toString());
90-
const output = () => data;
91-
92-
return new Promise<string>((resolve, reject) => (gitCommand(repoPath, capture, output, 'show', `${sha}:${source.replace(/\\/g, '/')}`) as Promise<Array<string>>).then(o => resolve(o.join())));
67+
export function gitGetVersionText(repoPath: string, sha: string, source: string) {
68+
return gitCommand(repoPath, 'show', `${sha}:${source.replace(/\\/g, '/')}`);
9369
}
9470

95-
function gitCommand(cwd: string, capture: (input: Buffer) => void, output: () => any, ...args): Promise<any> {
96-
return new Promise<any>((resolve, reject) => {
97-
let spawn = require('child_process').spawn;
98-
let process = spawn('git', args, { cwd: cwd });
99-
100-
process.stdout.on('data', data => {
101-
capture(data);
102-
});
103-
104-
let errors: Array<string> = [];
105-
process.stderr.on('data', err => {
106-
errors.push(err.toString());
107-
});
108-
109-
process.on('close', (exitCode, exitSignal) => {
110-
if (exitCode && errors.length) {
111-
reject(errors.toString());
112-
return;
113-
}
114-
115-
try {
116-
resolve(output());
117-
} catch (ex) {
118-
reject(ex);
119-
}
120-
});
121-
});
71+
function gitCommand(cwd: string, ...args) {
72+
return spawnPromise('git', args, { cwd: cwd });
12273
}

0 commit comments

Comments
 (0)