11 Commits

Author SHA1 Message Date
Eric Amodio
3a17605017 Preps v5.1.0 2017-09-15 21:03:52 -04:00
Eric Amodio
2c9a26e47b Fixes untracked files not showing in stash list 2017-09-15 18:12:22 -04:00
Eric Amodio
1c7785fd52 Adds note for closed issue in changelog 2017-09-15 17:39:39 -04:00
Eric Amodio
079f7b7f36 Switches to use a unicode arrow for the external link icon 2017-09-15 17:39:39 -04:00
Eric Amodio
bedc1a05f5 Preps v5.1.0-beta 2017-09-15 17:39:39 -04:00
Eric Amodio
858d9ec578 Fixes issue with stashes w/ only untracked files 2017-09-15 17:39:38 -04:00
Eric Amodio
2809991096 Closes #116 - adds full commit msg to annotations
Switches to use HoverProvider for hovers in file blames
2017-09-15 17:38:37 -04:00
Eric Amodio
f6019454b6 Adds open in remote to hover annotations
Optimizes annotation computation (cache by commit)
2017-09-14 23:43:41 -04:00
Eric Amodio
f0bdf3e2c3 Always caches remotes 2017-09-14 22:46:40 -04:00
Eric Amodio
0fdf856c27 Adds performance logging 2017-09-14 22:45:23 -04:00
Eric Amodio
aacf7cc2b5 Reworks date parsing, formatting etc for perf
Isolates moment.js
2017-09-14 21:52:51 -04:00
37 changed files with 372 additions and 263 deletions

View File

@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
## [5.1.0] - 2017-09-15
### Added
- Adds full (multi-line) commit message to the `details` hover annotations -- closes [#116](https://github.com/eamodio/vscode-gitlens/issues/116)
- Adds an external link icon to the `details` hover annotations to run the `Open Commit in Remote` command (`gitlens.openCommitInRemote`)
### Changed
- Optimizes performance of the providing blame annotations, especially for large files (saw a ~78% improvement on some files)
- Optimizes date handling (parsing and formatting) for better performance and reduced memory consumption
### Removed
- Removes `gitlens.annotations.file.recentChanges.hover.wholeLine` setting as it didn't really make sense
### Fixed
- Fixes an issue where stashes with only untracked files would not show in the `Stashes` node of the GitLens custom view
- Fixes an issue where stashes with untracked files would not show its untracked files in the GitLens custom view
## [5.0.0] - 2017-09-12
### Added
- Adds an all-new `GitLens` custom view to the Explorer activity

View File

@@ -25,6 +25,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line,
- Adds a `changes` (diff) hover annotation to the current line annotation, which provides **instant** access to the line's previous version ([optional](#line-blame-annotation-settings), on by default)
- Clicking on `Changes` will run the `Compare File Revisions` command (`gitlens.diffWith`)
- Clicking the current and previous commit ids will run the `Show Commit Details` command (`gitlens.showQuickCommitDetails`)
- Clicking on external link icon will run the the `Open Commit in Remote` command (`gitlens.openCommitInRemote`)
![Line Blame Annotations](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-line-blame-annotations.png)
@@ -331,7 +332,6 @@ GitLens is highly customizable and provides many configuration settings to allow
|`gitlens.recentChanges.file.lineHighlight.locations`|Specifies where the highlights of the recently changed lines will be shown<br />`gutter` - adds a gutter glyph<br />`line` - adds a full-line highlight background color<br />`overviewRuler` - adds a decoration to the overviewRuler (scroll bar)
|`gitlens.annotations.file.recentChanges.hover.details`|Specifies whether or not to provide a commit details hover annotation
|`gitlens.annotations.file.recentChanges.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotation
|`gitlens.annotations.file.recentChanges.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line
### Code Lens Settings

View File

@@ -1,6 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#7F4E7E" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
C
!
</text>
</svg>

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#6C6C6C" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
?
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -1,6 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#9B4F96" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
C
!
</text>
</svg>

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -0,0 +1,6 @@
<svg width="14px" height="14px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="#6C6C6C" x="0" y="0" width="100" height="100" rx="35" ry="35"/>
<text x="50" y="75" font-size="75" text-anchor="middle" style="font-family: Menlo, Monaco, Consolas, &quot;Droid Sans Mono&quot;, &quot;Inconsolata&quot;, &quot;Courier New&quot;, monospace, &quot;Droid Sans Fallback&quot;;" fill="white">
?
</text>
</svg>

After

Width:  |  Height:  |  Size: 431 B

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "gitlens",
"version": "5.0.0",
"version": "5.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "gitlens",
"version": "5.0.0",
"version": "5.1.0",
"author": {
"name": "Eric Amodio",
"email": "eamodio@gmail.com"
@@ -132,11 +132,6 @@
"default": true,
"description": "Specifies whether or not to provide a changes (diff) hover annotation"
},
"gitlens.annotations.file.recentChanges.hover.wholeLine": {
"type": "boolean",
"default": true,
"description": "Specifies whether or not to trigger hover annotations over the whole line"
},
"gitlens.annotations.line.hover.details": {
"type": "boolean",
"default": true,

View File

@@ -222,7 +222,7 @@ export class AnnotationController extends Disposable {
}
getProvider(editor: TextEditor | undefined): AnnotationProviderBase | undefined {
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return undefined;
if (editor === undefined || editor.document === undefined || !this.git.isEditorBlameable(editor)) return undefined;
return this._annotationProviders.get(editor.viewColumn || -1);
}
@@ -233,7 +233,7 @@ export class AnnotationController extends Disposable {
if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;
const currentProvider = this._annotationProviders.get(editor.viewColumn || -1);
if (currentProvider && TextEditorComparer.equals(currentProvider.editor, editor)) {
if (currentProvider !== undefined && TextEditorComparer.equals(currentProvider.editor, editor)) {
await currentProvider.selection(shaOrLine);
return true;
}

View File

@@ -1,10 +1,9 @@
import { Strings } from '../system';
import { Dates, Strings } from '../system';
import { DecorationInstanceRenderOptions, DecorationOptions, MarkdownString, ThemableDecorationRenderOptions } from 'vscode';
import { DiffWithCommand, ShowQuickCommitDetailsCommand } from '../commands';
import { DiffWithCommand, OpenCommitInRemoteCommand, ShowQuickCommitDetailsCommand } from '../commands';
import { IThemeConfig, themeDefaults } from '../configuration';
import { GlyphChars } from '../constants';
import { CommitFormatter, GitCommit, GitDiffChunkLine, GitService, GitUri, ICommitFormatOptions } from '../gitService';
import * as moment from 'moment';
interface IHeatmapConfig {
enabled: boolean;
@@ -28,13 +27,13 @@ const escapeMarkdownRegEx = /[`\>\#\*\_\-\+\.]/g;
export class Annotations {
static applyHeatmap(decoration: DecorationOptions, date: Date, now: moment.Moment) {
static applyHeatmap(decoration: DecorationOptions, date: Date, now: number) {
const color = this._getHeatmapColor(now, date);
(decoration.renderOptions!.before! as any).borderColor = color;
}
private static _getHeatmapColor(now: moment.Moment, date: Date) {
const days = now.diff(moment(date), 'days');
private static _getHeatmapColor(now: number, date: Date) {
const days = Dates.dateDaysFromNow(date, now);
if (days <= 2) return '#ffeca7';
if (days <= 7) return '#ffdd8c';
@@ -48,7 +47,7 @@ export class Annotations {
return '#793738';
}
static getHoverMessage(commit: GitCommit, dateFormat: string | null): MarkdownString {
static getHoverMessage(commit: GitCommit, dateFormat: string | null, hasRemotes: boolean): MarkdownString {
if (dateFormat === null) {
dateFormat = 'MMMM Do, YYYY h:MMa';
}
@@ -65,7 +64,11 @@ export class Annotations {
message = `\n\n> ${message}`;
}
const markdown = new MarkdownString(`[\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)}) &nbsp; __${commit.author}__, ${moment(commit.date).fromNow()} &nbsp; _(${moment(commit.date).format(dateFormat)})_${message}`);
const openInRemoteCommand = hasRemotes
? `${'&nbsp;'.repeat(3)} [\`${GlyphChars.ArrowUpRight}\`](${OpenCommitInRemoteCommand.getMarkdownCommandArgs(commit.sha)} "Open in Remote")`
: '';
const markdown = new MarkdownString(`[\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)} "Show Commit Details") &nbsp; __${commit.author}__, ${commit.fromNow()} &nbsp; _(${commit.formatDate(dateFormat)})_ ${openInRemoteCommand} &nbsp; ${message}`);
markdown.isTrusted = true;
return markdown;
}
@@ -75,8 +78,8 @@ export class Annotations {
const codeDiff = this._getCodeDiff(chunkLine);
const markdown = new MarkdownString(commit.isUncommitted
? `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)}) &nbsp; ${GlyphChars.Dash} &nbsp; _uncommitted_\n${codeDiff}`
: `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)}) &nbsp; ${GlyphChars.Dash} &nbsp; [\`${commit.previousShortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.previousSha!)}) ${GlyphChars.ArrowLeftRight} [\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)})\n${codeDiff}`);
? `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)} "Open Changes") &nbsp; ${GlyphChars.Dash} &nbsp; _uncommitted_\n${codeDiff}`
: `[\`Changes\`](${DiffWithCommand.getMarkdownCommandArgs(commit)} "Open Changes") &nbsp; ${GlyphChars.Dash} &nbsp; [\`${commit.previousShortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.previousSha!)} "Show Commit Details") ${GlyphChars.ArrowLeftRight} [\`${commit.shortSha}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(commit.sha)} "Show Commit Details")\n${codeDiff}`);
markdown.isTrusted = true;
return markdown;
}
@@ -98,8 +101,8 @@ export class Annotations {
} as DecorationOptions;
}
static detailsHover(commit: GitCommit, dateFormat: string | null): DecorationOptions {
const message = this.getHoverMessage(commit, dateFormat);
static detailsHover(commit: GitCommit, dateFormat: string | null, hasRemotes: boolean): DecorationOptions {
const message = this.getHoverMessage(commit, dateFormat, hasRemotes);
return {
hoverMessage: message
} as DecorationOptions;
@@ -164,11 +167,12 @@ export class Annotations {
} as IRenderOptions;
}
static hover(commit: GitCommit, renderOptions: IRenderOptions, heatmap: boolean, dateFormat: string | null): DecorationOptions {
return {
hoverMessage: this.getHoverMessage(commit, dateFormat),
renderOptions: heatmap ? { before: { ...renderOptions.before } } : undefined
static hover(commit: GitCommit, renderOptions: IRenderOptions, now: number): DecorationOptions {
const decoration = {
renderOptions: { before: { ...renderOptions.before } }
} as DecorationOptions;
this.applyHeatmap(decoration, commit.date, now);
return decoration;
}
static hoverRenderOptions(cfgTheme: IThemeConfig, heatmap: IHeatmapConfig): IRenderOptions {

View File

@@ -1,13 +1,15 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, Range, TextEditor, TextEditorDecorationType } from 'vscode';
import { CancellationToken, Disposable, ExtensionContext, Hover, HoverProvider, languages, Position, Range, TextDocument, TextEditor, TextEditorDecorationType } from 'vscode';
import { AnnotationProviderBase } from './annotationProvider';
import { GitBlame, GitService, GitUri } from '../gitService';
import { Annotations, endOfLineIndex } from './annotations';
import { GitBlame, GitCommit, GitService, GitUri } from '../gitService';
import { WhitespaceController } from './whitespaceController';
export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase {
export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase implements HoverProvider {
protected _blame: Promise<GitBlame | undefined>;
protected _hoverProviderDisposable: Disposable;
constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, protected git: GitService, protected uri: GitUri) {
super(context, editor, decoration, highlightDecoration, whitespaceController);
@@ -15,6 +17,11 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
this._blame = this.git.getBlameForFile(this.uri);
}
async clear() {
this._hoverProviderDisposable && this._hoverProviderDisposable.dispose();
super.clear();
}
async selection(shaOrLine?: string | number, blame?: GitBlame) {
if (!this.highlightDecoration) return;
@@ -56,6 +63,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
const blame = await this._blame;
return blame !== undefined && blame.lines.length !== 0;
}
protected async getBlame(requiresWhitespaceHack: boolean): Promise<GitBlame | undefined> {
let whitespacePromise: Promise<void> | undefined;
// HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- override whitespace (turn off)
@@ -64,18 +72,47 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
}
let blame: GitBlame | undefined;
if (whitespacePromise) {
if (whitespacePromise !== undefined) {
[blame] = await Promise.all([this._blame, whitespacePromise]);
}
else {
blame = await this._blame;
}
if (blame === undefined || !blame.lines.length) {
if (blame === undefined || blame.lines.length === 0) {
this.whitespaceController && await this.whitespaceController.restore();
return undefined;
}
return blame;
}
registerHoverProvider() {
this._hoverProviderDisposable = languages.registerHoverProvider({ pattern: this.uri.fsPath }, this);
}
async provideHover(document: TextDocument, position: Position, token: CancellationToken): Promise<Hover | undefined> {
// Avoid double annotations if we are showing the whole-file hover blame annotations
if (this._config.blame.line.enabled && this.editor.selection.start.line === position.line) return undefined;
const cfg = this._config.annotations.file.gutter;
if (!cfg.hover.wholeLine && position.character !== 0) return undefined;
const blame = await this.getBlame(true);
if (blame === undefined) return undefined;
const line = blame.lines[position.line - this.uri.offset];
const commit = blame.commits.get(line.sha);
if (commit === undefined) return undefined;
// Get the full commit message -- since blame only returns the summary
let logCommit: GitCommit | undefined = undefined;
if (!commit.isUncommitted) {
logCommit = await this.git.getLogCommit(commit.repoPath, commit.uri.fsPath, commit.sha);
}
const message = Annotations.getHoverMessage(logCommit || commit, this._config.defaultDateFormat, this.git.hasRemotes(commit.repoPath));
return new Hover(message, document.validateRange(new Range(position.line, 0, position.line, endOfLineIndex)));
}
}

View File

@@ -2,11 +2,11 @@
import { Strings } from '../system';
import { DecorationOptions, Range } from 'vscode';
import { FileAnnotationType } from './annotationController';
import { Annotations, endOfLineIndex } from './annotations';
import { Annotations } from './annotations';
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
import { GlyphChars } from '../constants';
import { GitBlameCommit, ICommitFormatOptions } from '../gitService';
import * as moment from 'moment';
import { Logger } from '../logger';
export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
@@ -16,7 +16,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
const blame = await this.getBlame(true);
if (blame === undefined) return false;
// console.time('Computing blame annotations...');
const start = process.hrtime();
const cfg = this._config.annotations.file.gutter;
@@ -32,59 +32,53 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
tokenOptions: tokenOptions
};
const now = moment();
const now = Date.now();
const offset = this.uri.offset;
const renderOptions = Annotations.gutterRenderOptions(this._config.theme, cfg.heatmap);
const dateFormat = this._config.defaultDateFormat;
const separateLines = this._config.theme.annotations.file.gutter.separateLines;
const decorations: DecorationOptions[] = [];
const document = this.document;
const decorationsMap: { [sha: string]: DecorationOptions | undefined } = Object.create(null);
let commit: GitBlameCommit | undefined;
let compacted = false;
let details: DecorationOptions | undefined;
let gutter: DecorationOptions | undefined;
let previousSha: string | undefined;
for (const l of blame.lines) {
commit = blame.commits.get(l.sha);
if (commit === undefined) continue;
const line = l.line + offset;
if (previousSha === l.sha) {
// Use a shallow copy of the previous decoration options
gutter = { ...gutter } as DecorationOptions;
if (cfg.compact && !compacted) {
// Since we are wiping out the contextText make sure to copy the objects
gutter.renderOptions = { ...gutter.renderOptions };
gutter.renderOptions.before = {
...gutter.renderOptions.before,
...{ contentText: GlyphChars.Space.repeat(Strings.getWidth(gutter.renderOptions!.before!.contentText!)) }
gutter.renderOptions = {
...gutter.renderOptions,
before: {
...gutter.renderOptions!.before,
contentText: GlyphChars.Space.repeat(Strings.getWidth(gutter.renderOptions!.before!.contentText!))
}
};
if (separateLines) {
gutter.renderOptions.dark = { ...gutter.renderOptions.dark };
gutter.renderOptions.dark.before = { ...gutter.renderOptions.dark.before, ...{ textDecoration: 'none' } };
gutter.renderOptions.light = { ...gutter.renderOptions.light };
gutter.renderOptions.light.before = { ...gutter.renderOptions.light.before, ...{ textDecoration: 'none' } };
gutter.renderOptions.dark = {
...gutter.renderOptions.dark,
before: { ...gutter.renderOptions.dark!.before, textDecoration: 'none' }
};
gutter.renderOptions.light = {
...gutter.renderOptions.light,
before: { ...gutter.renderOptions.light!.before, textDecoration: 'none' }
};
}
compacted = true;
}
const endIndex = document.lineAt(line).firstNonWhitespaceCharacterIndex;
gutter.range = new Range(line, 0, line, endIndex);
decorations.push(gutter);
gutter.range = new Range(line, 0, line, 0);
if (details !== undefined) {
details = { ...details } as DecorationOptions;
details.range = cfg.hover.wholeLine
? document.validateRange(new Range(line, 0, line, endOfLineIndex))
: gutter.range;
decorations.push(details);
}
decorations.push(gutter);
continue;
}
@@ -92,30 +86,43 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
compacted = false;
previousSha = l.sha;
gutter = decorationsMap[l.sha];
if (gutter !== undefined) {
gutter = {
...gutter,
range: new Range(line, 0, line, 0)
} as DecorationOptions;
decorations.push(gutter);
continue;
}
commit = blame.commits.get(l.sha);
if (commit === undefined) continue;
gutter = Annotations.gutter(commit, cfg.format, options, renderOptions);
if (cfg.heatmap.enabled) {
Annotations.applyHeatmap(gutter, commit.date, now);
}
const endIndex = document.lineAt(line).firstNonWhitespaceCharacterIndex;
gutter.range = new Range(line, 0, line, endIndex);
decorations.push(gutter);
gutter.range = new Range(line, 0, line, 0);
if (cfg.hover.details) {
details = Annotations.detailsHover(commit, dateFormat);
details.range = cfg.hover.wholeLine
? document.validateRange(new Range(line, 0, line, endOfLineIndex))
: gutter.range;
decorations.push(details);
}
decorations.push(gutter);
decorationsMap[l.sha] = gutter;
}
if (decorations.length) {
this.editor.setDecorations(this.decoration!, decorations);
}
// console.timeEnd('Computing blame annotations...');
const duration = process.hrtime(start);
Logger.log(`${(duration[0] * 1000) + Math.floor(duration[1] / 1000000)} ms to compute gutter blame annotations`);
if (cfg.hover.details) {
this.registerHoverProvider();
}
this.selection(shaOrLine, blame);
return true;

View File

@@ -1,63 +1,70 @@
'use strict';
import { DecorationOptions, Range } from 'vscode';
import { FileAnnotationType } from './annotationController';
import { Annotations, endOfLineIndex } from './annotations';
import { Annotations } from './annotations';
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
import { GitBlameCommit } from '../gitService';
import * as moment from 'moment';
import { Logger } from '../logger';
export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase {
async provideAnnotation(shaOrLine?: string | number): Promise<boolean> {
this.annotationType = FileAnnotationType.Hover;
const blame = await this.getBlame(this._config.annotations.file.hover.heatmap.enabled);
if (blame === undefined) return false;
// console.time('Computing blame annotations...');
const cfg = this._config.annotations.file.hover;
const now = moment();
const offset = this.uri.offset;
const renderOptions = Annotations.hoverRenderOptions(this._config.theme, cfg.heatmap);
const dateFormat = this._config.defaultDateFormat;
const blame = await this.getBlame(cfg.heatmap.enabled);
if (blame === undefined) return false;
const decorations: DecorationOptions[] = [];
const document = this.document;
if (cfg.heatmap.enabled) {
const start = process.hrtime();
let commit: GitBlameCommit | undefined;
let hover: DecorationOptions | undefined;
const now = Date.now();
const offset = this.uri.offset;
const renderOptions = Annotations.hoverRenderOptions(this._config.theme, cfg.heatmap);
for (const l of blame.lines) {
commit = blame.commits.get(l.sha);
if (commit === undefined) continue;
const decorations: DecorationOptions[] = [];
const decorationsMap: { [sha: string]: DecorationOptions } = Object.create(null);
const line = l.line + offset;
let commit: GitBlameCommit | undefined;
let hover: DecorationOptions | undefined;
hover = Annotations.hover(commit, renderOptions, cfg.heatmap.enabled, dateFormat);
for (const l of blame.lines) {
const line = l.line + offset;
hover = decorationsMap[l.sha];
if (hover !== undefined) {
hover = {
...hover,
range: new Range(line, 0, line, 0)
} as DecorationOptions;
decorations.push(hover);
continue;
}
commit = blame.commits.get(l.sha);
if (commit === undefined) continue;
hover = Annotations.hover(commit, renderOptions, now);
hover.range = new Range(line, 0, line, 0);
decorations.push(hover);
decorationsMap[l.sha] = hover;
if (cfg.wholeLine) {
hover.range = document.validateRange(new Range(line, 0, line, endOfLineIndex));
}
else {
const endIndex = document.lineAt(line).firstNonWhitespaceCharacterIndex;
hover.range = new Range(line, 0, line, endIndex);
}
if (cfg.heatmap.enabled) {
Annotations.applyHeatmap(hover, commit.date, now);
if (decorations.length) {
this.editor.setDecorations(this.decoration!, decorations);
}
decorations.push(hover);
const duration = process.hrtime(start);
Logger.log(`${(duration[0] * 1000) + Math.floor(duration[1] / 1000000)} ms to compute hover blame annotations`);
}
if (decorations.length) {
this.editor.setDecorations(this.decoration!, decorations);
}
// console.timeEnd('Computing blame annotations...');
this.registerHoverProvider();
this.selection(shaOrLine, blame);
return true;
}

View File

@@ -4,6 +4,7 @@ import { Annotations, endOfLineIndex } from './annotations';
import { FileAnnotationType } from './annotationController';
import { AnnotationProviderBase } from './annotationProvider';
import { GitService, GitUri } from '../gitService';
import { Logger } from '../logger';
export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
@@ -20,6 +21,8 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
const diff = await this.git.getDiffForFile(this.uri, commit.previousSha);
if (diff === undefined) return false;
const start = process.hrtime();
const cfg = this._config.annotations.file.recentChanges;
const dateFormat = this._config.defaultDateFormat;
@@ -34,16 +37,11 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
if (line.state === 'unchanged') continue;
let endingIndex = 0;
if (cfg.hover.details || cfg.hover.changes) {
endingIndex = cfg.hover.wholeLine ? endOfLineIndex : this.editor.document.lineAt(count).firstNonWhitespaceCharacterIndex;
}
const range = this.editor.document.validateRange(new Range(new Position(count, 0), new Position(count, endingIndex)));
const range = this.editor.document.validateRange(new Range(new Position(count, 0), new Position(count, endOfLineIndex)));
if (cfg.hover.details) {
decorators.push({
hoverMessage: Annotations.getHoverMessage(commit, dateFormat),
hoverMessage: Annotations.getHoverMessage(commit, dateFormat, this.git.hasRemotes(commit.repoPath)),
range: range
} as DecorationOptions);
}
@@ -62,6 +60,9 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
this.editor.setDecorations(this.highlightDecoration!, decorators);
const duration = process.hrtime(start);
Logger.log(`${(duration[0] * 1000) + Math.floor(duration[1] / 1000000)} ms to compute recent changes annotations`);
return true;
}

View File

@@ -12,6 +12,15 @@ export interface OpenCommitInRemoteCommandArgs {
export class OpenCommitInRemoteCommand extends ActiveEditorCommand {
static getMarkdownCommandArgs(sha: string): string;
static getMarkdownCommandArgs(args: OpenCommitInRemoteCommandArgs): string;
static getMarkdownCommandArgs(argsOrSha: OpenCommitInRemoteCommandArgs | string): string {
const args = typeof argsOrSha === 'string'
? { sha: argsOrSha }
: argsOrSha;
return super.getMarkdownCommandArgsCore<OpenCommitInRemoteCommandArgs>(Commands.OpenCommitInRemote, args);
}
constructor(private git: GitService) {
super(Commands.OpenCommitInRemote);
}

View File

@@ -251,7 +251,6 @@ export interface IConfig {
hover: {
details: boolean;
changes: boolean;
wholeLine: boolean;
};
};
};

View File

@@ -80,6 +80,7 @@ export type GlyphChars = '\u21a9' |
'\u2192' |
'\u21e8' |
'\u2191' |
'\u2197' |
'\u2713' |
'\u2014' |
'\u2022' |
@@ -95,6 +96,7 @@ export const GlyphChars = {
ArrowRight: '\u2192' as GlyphChars,
ArrowRightHollow: '\u21e8' as GlyphChars,
ArrowUp: '\u2191' as GlyphChars,
ArrowUpRight: '\u2197' as GlyphChars,
Check: '\u2713' as GlyphChars,
Dash: '\u2014' as GlyphChars,
Dot: '\u2022' as GlyphChars,

View File

@@ -295,12 +295,10 @@ export class CurrentLineController extends Disposable {
const decorationOptions: DecorationOptions[] = [];
let showChanges = false;
let showChangesStartIndex = 0;
let showChangesInStartingWhitespace = false;
let showDetails = false;
let showDetailsStartIndex = 0;
let showDetailsInStartingWhitespace = false;
let showAtStart = false;
let showStartIndex = 0;
switch (state.annotationType) {
case LineAnnotationType.Trailing: {
@@ -308,21 +306,7 @@ export class CurrentLineController extends Disposable {
showChanges = cfgAnnotations.hover.changes;
showDetails = cfgAnnotations.hover.details;
if (cfgAnnotations.hover.wholeLine) {
showChangesStartIndex = 0;
showChangesInStartingWhitespace = false;
showDetailsStartIndex = 0;
showDetailsInStartingWhitespace = false;
}
else {
showChangesStartIndex = endOfLineIndex;
showChangesInStartingWhitespace = true;
showDetailsStartIndex = endOfLineIndex;
showDetailsInStartingWhitespace = true;
}
showStartIndex = cfgAnnotations.hover.wholeLine ? 0 : endOfLineIndex;
const decoration = Annotations.trailing(commit, cfgAnnotations.format, cfgAnnotations.dateFormat === null ? this._config.defaultDateFormat : cfgAnnotations.dateFormat, this._config.theme);
decoration.range = editor.document.validateRange(new Range(line, endOfLineIndex, line, endOfLineIndex));
@@ -334,12 +318,8 @@ export class CurrentLineController extends Disposable {
const cfgAnnotations = this._config.annotations.line.hover;
showChanges = cfgAnnotations.changes;
showChangesStartIndex = 0;
showChangesInStartingWhitespace = false;
showDetails = cfgAnnotations.details;
showDetailsStartIndex = 0;
showDetailsInStartingWhitespace = false;
showStartIndex = 0;
break;
}
@@ -348,25 +328,15 @@ export class CurrentLineController extends Disposable {
if (showDetails || showChanges) {
const annotationType = this.annotationController.getAnnotationType(editor);
const firstNonWhitespace = editor.document.lineAt(line).firstNonWhitespaceCharacterIndex;
switch (annotationType) {
case FileAnnotationType.Gutter: {
const cfgHover = this._config.annotations.file.gutter.hover;
if (cfgHover.details) {
showDetailsInStartingWhitespace = false;
if (cfgHover.wholeLine) {
// Avoid double annotations if we are showing the whole-file hover blame annotations
showDetails = false;
showStartIndex = 0;
}
else {
if (showDetailsStartIndex === 0) {
showDetailsStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
if (showChangesStartIndex === 0) {
showChangesInStartingWhitespace = true;
showChangesStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
else if (showStartIndex !== 0) {
showAtStart = true;
}
}
@@ -374,20 +344,11 @@ export class CurrentLineController extends Disposable {
}
case FileAnnotationType.Hover: {
const cfgHover = this._config.annotations.file.hover;
showDetailsInStartingWhitespace = false;
if (cfgHover.wholeLine) {
// Avoid double annotations if we are showing the whole-file hover blame annotations
showDetails = false;
showChangesStartIndex = 0;
showStartIndex = 0;
}
else {
if (showDetailsStartIndex === 0) {
showDetailsStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
if (showChangesStartIndex === 0) {
showChangesInStartingWhitespace = true;
showChangesStartIndex = firstNonWhitespace === 0 ? 1 : firstNonWhitespace;
}
else if (showStartIndex !== 0) {
showAtStart = true;
}
break;
@@ -395,29 +356,21 @@ export class CurrentLineController extends Disposable {
case FileAnnotationType.RecentChanges: {
const cfgChanges = this._config.annotations.file.recentChanges.hover;
if (cfgChanges.details) {
if (cfgChanges.wholeLine) {
// Avoid double annotations if we are showing the whole-file hover blame annotations
showDetails = false;
}
else {
showDetailsInStartingWhitespace = false;
}
// Avoid double annotations if we are showing the whole-file hover blame annotations
showDetails = false;
}
if (cfgChanges.changes) {
if (cfgChanges.wholeLine) {
// Avoid double annotations if we are showing the whole-file hover blame annotations
showChanges = false;
}
else {
showChangesInStartingWhitespace = false;
}
// Avoid double annotations if we are showing the whole-file hover blame annotations
showChanges = false;
}
break;
}
}
const range = editor.document.validateRange(new Range(line, showStartIndex, line, endOfLineIndex));
if (showDetails) {
// Get the full commit message -- since blame only returns the summary
let logCommit: GitCommit | undefined = undefined;
@@ -425,29 +378,22 @@ export class CurrentLineController extends Disposable {
logCommit = await this.git.getLogCommit(this._uri.repoPath, this._uri.fsPath, commit.sha);
}
// I have no idea why I need this protection -- but it happens
if (editor.document === undefined) return;
const decoration = Annotations.detailsHover(logCommit || commit, this._config.defaultDateFormat);
decoration.range = editor.document.validateRange(new Range(line, showDetailsStartIndex, line, endOfLineIndex));
const decoration = Annotations.detailsHover(logCommit || commit, this._config.defaultDateFormat, this.git.hasRemotes((logCommit || commit).repoPath));
decoration.range = range;
decorationOptions.push(decoration);
if (showDetailsInStartingWhitespace && showDetailsStartIndex !== 0 && decoration.range.end.character !== 0) {
decorationOptions.push(Annotations.withRange(decoration, 0, firstNonWhitespace));
if (showAtStart) {
decorationOptions.push(Annotations.withRange(decoration, 0, 0));
}
}
if (showChanges) {
const decoration = await Annotations.changesHover(commit, line, this._uri, this.git);
// I have no idea why I need this protection -- but it happens
if (editor.document === undefined) return;
decoration.range = editor.document.validateRange(new Range(line, showChangesStartIndex, line, endOfLineIndex));
decoration.range = range;
decorationOptions.push(decoration);
if (showChangesInStartingWhitespace && showChangesStartIndex !== 0 && decoration.range.end.character !== 0) {
decorationOptions.push(Annotations.withRange(decoration, 0, firstNonWhitespace));
if (showAtStart) {
decorationOptions.push(Annotations.withRange(decoration, 0, 0));
}
}
}

View File

@@ -2,7 +2,6 @@
import { Strings } from '../../system';
import { GitCommit } from '../models/commit';
import { Formatter, IFormatOptions } from './formatter';
import * as moment from 'moment';
import { GlyphChars } from '../../constants';
export interface ICommitFormatOptions extends IFormatOptions {
@@ -20,7 +19,7 @@ export interface ICommitFormatOptions extends IFormatOptions {
export class CommitFormatter extends Formatter<GitCommit, ICommitFormatOptions> {
get ago() {
const ago = moment(this._item.date).fromNow();
const ago = this._item.fromNow();
return this._padOrTruncate(ago, this._options.tokenOptions!.ago);
}
@@ -30,12 +29,12 @@ export class CommitFormatter extends Formatter<GitCommit, ICommitFormatOptions>
}
get authorAgo() {
const authorAgo = `${this._item.author}, ${moment(this._item.date).fromNow()}`;
const authorAgo = `${this._item.author}, ${this._item.fromNow()}`;
return this._padOrTruncate(authorAgo, this._options.tokenOptions!.authorAgo);
}
get date() {
const date = moment(this._item.date).format(this._options.dateFormat!);
const date = this._item.formatDate(this._options.dateFormat!);
return this._padOrTruncate(date, this._options.tokenOptions!.date);
}

View File

@@ -21,9 +21,9 @@ export * from './remotes/provider';
let git: IGit;
// `--format=%H -%nauthor %an%nauthor-date %ai%ncommitter %cn%ncommitter-date %ci%nparents %P%nsummary %B%nfilename ?`
const defaultLogParams = [`log`, `--name-status`, `--full-history`, `-M`, `--date=iso8601`, `--format=%H -%nauthor %an%nauthor-date %ai%nparents %P%nsummary %B%nfilename ?`];
const defaultStashParams = [`stash`, `list`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor-date %ai%nreflog-selector %gd%nsummary %B%nfilename ?`];
const defaultBlameParams = [`blame`, `--root`, `--incremental`];
const defaultLogParams = [`log`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor %an%nauthor-date %at%nparents %P%nsummary %B%nfilename ?`];
const defaultStashParams = [`stash`, `list`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor-date %at%nreflog-selector %gd%nsummary %B%nfilename ?`];
let defaultEncoding = 'utf8';
export function setDefaultEncoding(encoding: string) {
@@ -180,7 +180,7 @@ export class Git {
static blame(repoPath: string | undefined, fileName: string, sha?: string, options: { ignoreWhitespace?: boolean, startLine?: number, endLine?: number } = {}) {
const [file, root] = Git.splitPath(fileName, repoPath);
const params = [`blame`, `--root`, `--incremental`];
const params = [...defaultBlameParams];
if (options.ignoreWhitespace) {
params.push('-w');

View File

@@ -42,7 +42,7 @@ export class GitUri extends Uri {
}
else {
const commit = commitOrRepoPath;
base._fsPath = path.resolve(commit.repoPath, commit.originalFileName || commit.fileName);
base._fsPath = path.resolve(commit.repoPath, commit.originalFileName || commit.fileName || '');
if (commit.repoPath !== undefined) {
this.repoPath = commit.repoPath;

View File

@@ -1,5 +1,5 @@
'use strict';
import { Strings } from '../../system';
import { Dates, Strings } from '../../system';
import { Uri } from 'vscode';
import { GlyphChars } from '../../constants';
import { Git } from '../git';
@@ -70,7 +70,23 @@ export class GitCommit {
}
get uri(): Uri {
return Uri.file(path.resolve(this.repoPath, this.originalFileName || this.fileName));
return Uri.file(path.resolve(this.repoPath, this.originalFileName || this.fileName || ''));
}
private _dateFormatter?: Dates.IDateFormatter;
formatDate(format: string) {
if (this._dateFormatter === undefined) {
this._dateFormatter = Dates.toFormatter(this.date);
}
return this._dateFormatter.format(format);
}
fromNow() {
if (this._dateFormatter === undefined) {
this._dateFormatter = Dates.toFormatter(this.date);
}
return this._dateFormatter.fromNow();
}
getFormattedPath(separator: string = Strings.pad(GlyphChars.Dot, 2, 2)): string {

View File

@@ -40,7 +40,12 @@ export class GitLogCommit extends GitCommit {
this.status = fileStatus.status;
}
else {
this.fileStatuses = [{ status: status, fileName: fileName, originalFileName: originalFileName } as IGitStatusFile];
if (fileName === undefined) {
this.fileStatuses = [];
}
else {
this.fileStatuses = [{ status: status, fileName: fileName, originalFileName: originalFileName } as IGitStatusFile];
}
this.status = status;
}
}

View File

@@ -19,7 +19,7 @@ export interface GitStatus {
files: GitStatusFile[];
}
export declare type GitStatusFileStatus = '!' | '?' | 'A' | 'C' | 'D' | 'M' | 'R' | 'U';
export declare type GitStatusFileStatus = '!' | '?' | 'A' | 'C' | 'D' | 'M' | 'R' | 'T' | 'U' | 'X' | 'B';
export interface IGitStatusFile {
status: GitStatusFileStatus;
@@ -71,7 +71,10 @@ const statusOcticonsMap = {
D: '$(diff-removed)',
M: '$(diff-modified)',
R: '$(diff-renamed)',
U: '$(question)'
T: '$(diff-modified)',
U: '$(alert)',
X: '$(question)',
B: '$(question)'
};
export function getGitStatusOcticon(status: GitStatusFileStatus, missing: string = GlyphChars.Space.repeat(4)): string {
@@ -86,7 +89,10 @@ const statusIconsMap = {
D: 'icon-status-deleted.svg',
M: 'icon-status-modified.svg',
R: 'icon-status-renamed.svg',
U: 'icon-status-conflict.svg'
T: 'icon-status-modified.svg',
U: 'icon-status-conflict.svg',
X: 'icon-status-unknown.svg',
B: 'icon-status-unknown.svg'
};
export function getGitStatusIcon(status: GitStatusFileStatus): string {

View File

@@ -1,7 +1,6 @@
'use strict';
import { Strings } from '../../system';
import { Git, GitAuthor, GitBlame, GitBlameCommit, GitCommitLine } from './../git';
import * as moment from 'moment';
import * as path from 'path';
interface BlameEntry {
@@ -134,7 +133,7 @@ export class GitBlameParser {
}
}
commit = new GitBlameCommit(repoPath!, entry.sha, fileName!, entry.author, moment(`${entry.authorDate} ${entry.authorTimeZone}`, 'X +-HHmm').toDate(), entry.summary!, []);
commit = new GitBlameCommit(repoPath!, entry.sha, fileName!, entry.author, new Date(entry.authorDate as any * 1000), entry.summary!, []);
if (fileName !== entry.fileName) {
commit.originalFileName = entry.fileName;

View File

@@ -3,7 +3,6 @@ import { Strings } from '../../system';
import { Range } from 'vscode';
import { Git, GitAuthor, GitCommitType, GitLog, GitLogCommit, GitStatusFileStatus, IGitStatusFile } from './../git';
// import { Logger } from '../../logger';
import * as moment from 'moment';
import * as path from 'path';
interface LogEntry {
@@ -87,7 +86,7 @@ export class GitLogParser {
break;
case 'author-date':
entry.authorDate = `${lineParts[1]}T${lineParts[2]}${lineParts[3]}`;
entry.authorDate = lineParts[1];
break;
case 'parents':
@@ -231,7 +230,7 @@ export class GitLogParser {
}
}
commit = new GitLogCommit(type, repoPath!, entry.sha, relativeFileName, entry.author, moment(entry.authorDate).toDate(), entry.summary!, entry.status, entry.fileStatuses, undefined, entry.originalFileName);
commit = new GitLogCommit(type, repoPath!, entry.sha, relativeFileName, entry.author, new Date(entry.authorDate! as any * 1000), entry.summary!, entry.status, entry.fileStatuses, undefined, entry.originalFileName);
commit.parentShas = entry.parentShas!;
if (relativeFileName !== entry.fileName) {

View File

@@ -1,7 +1,6 @@
'use strict';
import { Git, GitStash, GitStashCommit, GitStatusFileStatus, IGitStatusFile } from './../git';
// import { Logger } from '../../logger';
import * as moment from 'moment';
interface StashEntry {
sha: string;
@@ -14,11 +13,33 @@ interface StashEntry {
export class GitStashParser {
static parse(data: string, repoPath: string): GitStash | undefined {
const entries = this._parseEntries(data);
if (entries === undefined) return undefined;
const commits: Map<string, GitStashCommit> = new Map();
for (let i = 0, len = entries.length; i < len; i++) {
const entry = entries[i];
let commit = commits.get(entry.sha);
if (commit === undefined) {
commit = new GitStashCommit(entry.stashName, repoPath, entry.sha, entry.fileNames, new Date(entry.date! as any * 1000), entry.summary, undefined, entry.fileStatuses) as GitStashCommit;
commits.set(entry.sha, commit);
}
}
return {
repoPath: repoPath,
commits: commits
} as GitStash;
}
private static _parseEntries(data: string): StashEntry[] | undefined {
if (!data) return undefined;
const lines = data.split('\n');
if (!lines.length) return undefined;
if (lines.length === 0) return undefined;
const entries: StashEntry[] = [];
@@ -42,7 +63,7 @@ export class GitStashParser {
switch (lineParts[0]) {
case 'author-date':
entry.date = `${lineParts[1]}T${lineParts[2]}${lineParts[3]}`;
entry.date = lineParts[1];
break;
case 'summary':
@@ -66,7 +87,12 @@ export class GitStashParser {
case 'filename':
const nextLine = lines[position + 1];
// If the next line isn't blank, make sure it isn't starting a new commit
if (nextLine && Git.shaRegex.test(nextLine)) continue;
if (nextLine && Git.shaRegex.test(nextLine)) {
entries.push(entry);
entry = undefined;
continue;
}
position++;
@@ -109,28 +135,6 @@ export class GitStashParser {
return entries;
}
static parse(data: string, repoPath: string): GitStash | undefined {
const entries = this._parseEntries(data);
if (entries === undefined) return undefined;
const commits: Map<string, GitStashCommit> = new Map();
for (let i = 0, len = entries.length; i < len; i++) {
const entry = entries[i];
let commit = commits.get(entry.sha);
if (commit === undefined) {
commit = new GitStashCommit(entry.stashName, repoPath, entry.sha, entry.fileNames, moment(entry.date).toDate(), entry.summary, undefined, entry.fileStatuses) as GitStashCommit;
commits.set(entry.sha, commit);
}
}
return {
repoPath: repoPath,
commits: commits
} as GitStash;
}
private static _parseFileName(entry: { fileName?: string, originalFileName?: string }) {
if (entry.fileName === undefined) return;

View File

@@ -17,7 +17,7 @@ export class GitStatusParser {
if (!data) return undefined;
const lines = data.split('\n').filter(_ => !!_);
if (!lines.length) return undefined;
if (lines.length === 0) return undefined;
const status = {
branch: '',

View File

@@ -6,7 +6,6 @@ import { BuiltInCommands, DocumentSchemes, ExtensionKey } from './constants';
import { CodeLensCommand, CodeLensLocations, ICodeLensLanguageLocation, IConfig } from './configuration';
import { GitBlame, GitBlameCommit, GitBlameLines, GitService, GitUri } from './gitService';
import { Logger } from './logger';
import * as moment from 'moment';
export class GitRecentChangeCodeLens extends CodeLens {
@@ -254,7 +253,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
if (blame === undefined) return lens;
const recentCommit = Iterables.first(blame.commits.values());
title = `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`;
title = `${recentCommit.author}, ${recentCommit.fromNow()}`;
if (this._config.codeLens.debug) {
title += ` [${SymbolKind[lens.symbolKind]}(${lens.range.start.character}-${lens.range.end.character}), Lines (${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1}), Commit (${recentCommit.shortSha})]`;
}

View File

@@ -9,7 +9,6 @@ import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri';
import { Logger } from './logger';
import * as fs from 'fs';
import * as ignore from 'ignore';
import * as moment from 'moment';
import * as path from 'path';
export { GitUri, IGitCommitInfo };
@@ -167,7 +166,6 @@ export class GitService extends Disposable {
this._repoWatcher = undefined;
this._gitCache.clear();
this._remotesCache.clear();
}
this._gitignore = new Promise<ignore.Ignore | undefined>((resolve, reject) => {
@@ -568,7 +566,7 @@ export class GitService extends Disposable {
Iterables.forEach(blame.commits.values(), (c, i) => {
if (c.isUncommitted) return;
const decoration = `${GlyphChars.ArrowDropRight} ${c.author}, ${moment(c.date).format(dateFormat)}`;
const decoration = `${GlyphChars.ArrowDropRight} ${c.author}, ${c.formatDate(dateFormat)}`;
const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration, dateFormat);
locations.push(new Location(uri, new Position(0, 0)));
if (c.sha === selectedSha) {
@@ -888,7 +886,7 @@ export class GitService extends Disposable {
Iterables.forEach(log.commits.values(), (c, i) => {
if (c.isUncommitted) return;
const decoration = `${GlyphChars.ArrowDropRight} ${c.author}, ${moment(c.date).format(dateFormat)}`;
const decoration = `${GlyphChars.ArrowDropRight} ${c.author}, ${c.formatDate(dateFormat)}`;
const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration, dateFormat);
locations.push(new Location(uri, new Position(0, 0)));
if (c.sha === selectedSha) {
@@ -899,21 +897,26 @@ export class GitService extends Disposable {
return locations;
}
hasRemotes(repoPath: string): boolean {
const remotes = this._remotesCache.get(repoPath);
return remotes !== undefined && remotes.length > 0;
}
async getRemotes(repoPath: string): Promise<GitRemote[]> {
if (!repoPath) return [];
Logger.log(`getRemotes('${repoPath}')`);
if (this.UseCaching) {
const remotes = this._remotesCache.get(repoPath);
if (remotes !== undefined) return remotes;
}
let remotes = this._remotesCache.get(repoPath);
if (remotes !== undefined) return remotes;
const data = await Git.remote(repoPath);
const remotes = GitRemoteParser.parse(data, repoPath);
if (this.UseCaching) {
remotes = GitRemoteParser.parse(data, repoPath);
if (remotes !== undefined) {
this._remotesCache.set(repoPath, remotes);
}
return remotes;
}
@@ -1138,7 +1141,7 @@ export class GitService extends Disposable {
}
// NOTE: Need to specify an index here, since I can't control the sort order -- just alphabetic or by file location
return Uri.parse(`${scheme}:${pad(data.index || 0)} ${GlyphChars.Dot} ${encodeURIComponent(message)} ${GlyphChars.Dot} ${moment(commit.date).format(dateFormat)} ${GlyphChars.Dot} ${encodeURIComponent(uriPath)}?${JSON.stringify(data)}`);
return Uri.parse(`${scheme}:${pad(data.index || 0)} ${GlyphChars.Dot} ${encodeURIComponent(message)} ${GlyphChars.Dot} ${commit.formatDate(dateFormat)} ${GlyphChars.Dot} ${encodeURIComponent(uriPath)}?${JSON.stringify(data)}`);
}
private static _toGitUriData<T extends IGitUriData>(commit: IGitUriData, index?: number, originalFileName?: string, decoration?: string): T {

View File

@@ -3,7 +3,6 @@ import { commands, ExtensionContext, Uri, window } from 'vscode';
import { BuiltInCommands } from './constants';
import { GitCommit } from './gitService';
import { Logger } from './logger';
import * as moment from 'moment';
export type SuppressedKeys = 'suppressCommitHasNoPreviousCommitWarning' |
'suppressCommitNotFoundWarning' |
@@ -34,7 +33,7 @@ export class Messages {
static showCommitHasNoPreviousCommitWarningMessage(commit?: GitCommit): Promise<string | undefined> {
if (commit === undefined) return Messages._showMessage('info', `Commit has no previous commit`, SuppressedKeys.CommitHasNoPreviousCommitWarning);
return Messages._showMessage('info', `Commit ${commit.shortSha} (${commit.author}, ${moment(commit.date).fromNow()}) has no previous commit`, SuppressedKeys.CommitHasNoPreviousCommitWarning);
return Messages._showMessage('info', `Commit ${commit.shortSha} (${commit.author}, ${commit.fromNow()}) has no previous commit`, SuppressedKeys.CommitHasNoPreviousCommitWarning);
}
static showCommitNotFoundWarningMessage(message: string): Promise<string | undefined> {

View File

@@ -7,7 +7,6 @@ import { GlyphChars } from '../constants';
import { getGitStatusOcticon, GitCommit, GitLog, GitLogCommit, GitService, GitStashCommit, GitStatusFile, GitStatusFileStatus, GitUri, IGitCommitInfo, IGitStatusFile, RemoteResource } from '../gitService';
import { Keyboard, KeyCommand, KeyNoopCommand, Keys } from '../keyboard';
import { OpenRemotesCommandQuickPickItem } from './remotes';
import * as moment from 'moment';
import * as path from 'path';
export class CommitWithFileStatusQuickPickItem extends OpenFileCommandQuickPickItem {
@@ -299,7 +298,7 @@ export class CommitDetailsQuickPick {
const pick = await window.showQuickPick(items, {
matchOnDescription: true,
matchOnDetail: true,
placeHolder: `${commit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.author ? `${commit.author}, ` : ''}${moment(commit.date).fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.message}`,
placeHolder: `${commit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.author ? `${commit.author}, ` : ''}${commit.fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.message}`,
ignoreFocusOut: getQuickPickIgnoreFocusOut(),
onDidSelectItem: (item: QuickPickItem) => {
scope.setKeyCommand('right', item);

View File

@@ -7,7 +7,6 @@ import { GlyphChars } from '../constants';
import { GitLog, GitLogCommit, GitService, GitUri, RemoteResource } from '../gitService';
import { Keyboard, KeyCommand, KeyNoopCommand } from '../keyboard';
import { OpenRemotesCommandQuickPickItem } from './remotes';
import * as moment from 'moment';
import * as path from 'path';
export class OpenCommitFileCommandQuickPickItem extends OpenFileCommandQuickPickItem {
@@ -275,7 +274,7 @@ export class CommitFileDetailsQuickPick {
const pick = await window.showQuickPick(items, {
matchOnDescription: true,
placeHolder: `${commit.getFormattedPath()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${isUncommitted ? `Uncommitted ${GlyphChars.ArrowRightHollow} ` : '' }${commit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.author}, ${moment(commit.date).fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.message}`,
placeHolder: `${commit.getFormattedPath()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${isUncommitted ? `Uncommitted ${GlyphChars.ArrowRightHollow} ` : '' }${commit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.author}, ${commit.fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.message}`,
ignoreFocusOut: getQuickPickIgnoreFocusOut(),
onDidSelectItem: (item: QuickPickItem) => {
scope.setKeyCommand('right', item as KeyCommand);

View File

@@ -7,7 +7,6 @@ import { GlyphChars } from '../constants';
import { GitCommit, GitLogCommit, GitStashCommit } from '../gitService';
import { Keyboard, KeyboardScope, KeyMapping, Keys } from '../keyboard';
// import { Logger } from '../logger';
import * as moment from 'moment';
export function getQuickPickIgnoreFocusOut() {
const cfg = workspace.getConfiguration(ExtensionKey).get<IAdvancedConfig>('advanced')!;
@@ -174,12 +173,12 @@ export class CommitQuickPickItem implements QuickPickItem {
if (commit instanceof GitStashCommit) {
this.label = message;
this.description = '';
this.detail = `${GlyphChars.Space} ${commit.stashName} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${moment(commit.date).fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.getDiffStatus()}`;
this.detail = `${GlyphChars.Space} ${commit.stashName} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.fromNow()} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.getDiffStatus()}`;
}
else {
this.label = message;
this.description = `${Strings.pad('$(git-commit)', 1, 1)} ${commit.shortSha}`;
this.detail = `${GlyphChars.Space} ${commit.author}, ${moment(commit.date).fromNow()}${(commit.type === 'branch') ? ` ${Strings.pad(GlyphChars.Dot, 1, 1)} ${(commit as GitLogCommit).getDiffStatus()}` : ''}`;
this.detail = `${GlyphChars.Space} ${commit.author}, ${commit.fromNow()}${(commit.type === 'branch') ? ` ${Strings.pad(GlyphChars.Dot, 1, 1)} ${(commit as GitLogCommit).getDiffStatus()}` : ''}`;
}
}
}

View File

@@ -1,5 +1,6 @@
'use strict';
export * from './system/array';
export * from './system/date';
// export * from './system/disposable';
// export * from './system/element';
// export * from './system/event';

33
src/system/date.ts Normal file
View File

@@ -0,0 +1,33 @@
'use strict';
import * as moment from 'moment';
const MillisecondsPerMinute = 60000; // 60 * 1000
const MillisecondsPerDay = 86400000; // 24 * 60 * 60 * 1000
export namespace Dates {
export interface IDateFormatter {
fromNow: () => string;
format: (format: string) => string;
}
export function dateDaysFromNow(date: Date, now: number = Date.now()) {
const startOfDayLeft = startOfDay(now);
const startOfDayRight = startOfDay(date);
const timestampLeft = startOfDayLeft.getTime() - startOfDayLeft.getTimezoneOffset() * MillisecondsPerMinute;
const timestampRight = startOfDayRight.getTime() - startOfDayRight.getTimezoneOffset() * MillisecondsPerMinute;
return Math.round((timestampLeft - timestampRight) / MillisecondsPerDay);
}
export function startOfDay(date: Date | number) {
const newDate = new Date(typeof date === 'number' ? date : date.getTime());
newDate.setHours(0, 0, 0, 0);
return newDate;
}
export function toFormatter(date: Date): IDateFormatter {
return moment(date);
}
}

View File

@@ -1,4 +1,5 @@
'use strict';
import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerNode, ResourceType } from './explorerNode';
import { CommitFormatter, GitService, GitStashCommit, GitUri, ICommitFormatOptions } from '../gitService';
@@ -13,7 +14,20 @@ export class StashNode extends ExplorerNode {
}
async getChildren(): Promise<ExplorerNode[]> {
return Promise.resolve((this.commit as GitStashCommit).fileStatuses.map(s => new StashFileNode(s, this.commit, this.context, this.git)));
const statuses = (this.commit as GitStashCommit).fileStatuses;
// Check for any untracked files -- since git doesn't return them via `git stash list` :(
const log = await this.git.getLogForRepo(this.commit.repoPath, `${(this.commit as GitStashCommit).stashName}^3`, 1);
if (log !== undefined) {
const commit = Iterables.first(log.commits.values());
if (commit !== undefined && commit.fileStatuses.length !== 0) {
// Since these files are untracked -- make them look that way
commit.fileStatuses.forEach(s => s.status = '?');
statuses.splice(statuses.length, 0, ...commit.fileStatuses);
}
}
return Promise.resolve(statuses.map(s => new StashFileNode(s, this.commit, this.context, this.git)));
}
getTreeItem(): TreeItem {