Browse Source

Closes #493 - Adds changes to commits in explorers

Adds new ${changes} and ${changesShort} template tokens
Adds support for prefixes and suffixes around tokens
Fixes missing/mismatched token option issues
main
Eric Amodio 6 years ago
parent
commit
2d977262d2
11 changed files with 212 additions and 104 deletions
  1. +2
    -2
      package.json
  2. +26
    -3
      src/git/formatters/commitFormatter.ts
  3. +49
    -34
      src/git/formatters/formatter.ts
  4. +6
    -2
      src/git/formatters/statusFormatter.ts
  5. +63
    -18
      src/git/models/logCommit.ts
  6. +42
    -32
      src/git/models/status.ts
  7. +1
    -1
      src/quickpicks/commitQuickPick.ts
  8. +2
    -2
      src/quickpicks/commonQuickPicks.ts
  9. +11
    -8
      src/system/string.ts
  10. +8
    -0
      src/views/nodes/commitNode.ts
  11. +2
    -2
      src/views/nodes/statusNode.ts

+ 2
- 2
package.json View File

@ -451,7 +451,7 @@
}, },
"gitlens.explorers.commitFormat": { "gitlens.explorers.commitFormat": {
"type": "string", "type": "string",
"default": "${message} • ${authorAgoOrDate} (${id})",
"default": "${message} • ${authorAgoOrDate}${ (id)}",
"description": "Specifies the format of committed changes in the `GitLens` and `GitLens Results` explorers\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.defaultDateFormat`)\\n ${agoOrDate} - commit date specified by `gitlens.defaultDateStyle`\n ${authorAgo} - commit author, relative commit date\n ${authorAgoOrDate} - commit author, commit date specified by `gitlens.defaultDateStyle`\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting", "description": "Specifies the format of committed changes in the `GitLens` and `GitLens Results` explorers\nAvailable tokens\n ${id} - commit id\n ${author} - commit author\n ${message} - commit message\n ${ago} - relative commit date (e.g. 1 day ago)\n ${date} - formatted commit date (format specified by `gitlens.defaultDateFormat`)\\n ${agoOrDate} - commit date specified by `gitlens.defaultDateStyle`\n ${authorAgo} - commit author, relative commit date\n ${authorAgoOrDate} - commit author, commit date specified by `gitlens.defaultDateStyle`\nSee https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting",
"scope": "window" "scope": "window"
}, },
@ -469,7 +469,7 @@
}, },
"gitlens.explorers.statusFileFormat": { "gitlens.explorers.statusFileFormat": {
"type": "string", "type": "string",
"default": "${working}${filePath}",
"default": "${working }${filePath}",
"description": "Specifies the format of the status of a working or committed file in the `GitLens` and `GitLens Results` explorers\nAvailable tokens\n ${directory} - directory name\n ${file} - file name\n ${filePath} - formatted file name and path\n ${path} - full file path\n ${working} - optional indicator if the file is uncommitted", "description": "Specifies the format of the status of a working or committed file in the `GitLens` and `GitLens Results` explorers\nAvailable tokens\n ${directory} - directory name\n ${file} - file name\n ${filePath} - formatted file name and path\n ${path} - full file path\n ${working} - optional indicator if the file is uncommitted",
"scope": "window" "scope": "window"
}, },

+ 26
- 3
src/git/formatters/commitFormatter.ts View File

@ -3,7 +3,8 @@ import { DateStyle } from '../../configuration';
import { GlyphChars } from '../../constants'; import { GlyphChars } from '../../constants';
import { Container } from '../../container'; import { Container } from '../../container';
import { Strings } from '../../system'; import { Strings } from '../../system';
import { GitCommit } from '../models/commit';
import { GitCommit, GitCommitType } from '../models/commit';
import { GitLogCommit } from '../models/models';
import { Formatter, IFormatOptions } from './formatter'; import { Formatter, IFormatOptions } from './formatter';
const emojiMap: { [key: string]: string } = require('../../../emoji/emojis.json'); const emojiMap: { [key: string]: string } = require('../../../emoji/emojis.json');
@ -19,7 +20,10 @@ export interface ICommitFormatOptions extends IFormatOptions {
author?: Strings.ITokenOptions; author?: Strings.ITokenOptions;
authorAgo?: Strings.ITokenOptions; authorAgo?: Strings.ITokenOptions;
authorAgoOrDate?: Strings.ITokenOptions; authorAgoOrDate?: Strings.ITokenOptions;
changes?: Strings.ITokenOptions;
changesShort?: Strings.ITokenOptions;
date?: Strings.ITokenOptions; date?: Strings.ITokenOptions;
id?: Strings.ITokenOptions;
message?: Strings.ITokenOptions; message?: Strings.ITokenOptions;
}; };
} }
@ -59,7 +63,26 @@ export class CommitFormatter extends Formatter
get authorAgoOrDate() { get authorAgoOrDate() {
const authorAgo = `${this._item.author}, ${this._agoOrDate}`; const authorAgo = `${this._item.author}, ${this._agoOrDate}`;
return this._padOrTruncate(authorAgo, this._options.tokenOptions!.authorAgo);
return this._padOrTruncate(authorAgo, this._options.tokenOptions!.authorAgoOrDate);
}
get changes() {
if (!(this._item instanceof GitLogCommit) || this._item.type === GitCommitType.File) {
return this._padOrTruncate('', this._options.tokenOptions!.changes);
}
return this._padOrTruncate(this._item.getFormattedDiffStatus(), this._options.tokenOptions!.changes);
}
get changesShort() {
if (!(this._item instanceof GitLogCommit) || this._item.type === GitCommitType.File) {
return this._padOrTruncate('', this._options.tokenOptions!.changesShort);
}
return this._padOrTruncate(
this._item.getFormattedDiffStatus({ compact: true, separator: '' }),
this._options.tokenOptions!.changesShort
);
} }
get date() { get date() {
@ -67,7 +90,7 @@ export class CommitFormatter extends Formatter
} }
get id() { get id() {
return this._item.shortSha;
return this._padOrTruncate(this._item.shortSha || '', this._options.tokenOptions!.id);
} }
get message() { get message() {

+ 49
- 34
src/git/formatters/formatter.ts View File

@ -41,50 +41,62 @@ export abstract class Formatter
private collapsableWhitespace: number = 0; private collapsableWhitespace: number = 0;
protected _padOrTruncate(s: string, options: Strings.ITokenOptions | undefined) { protected _padOrTruncate(s: string, options: Strings.ITokenOptions | undefined) {
if (s === '') return s;
// NOTE: the collapsable whitespace logic relies on the javascript template evaluation to be left to right // NOTE: the collapsable whitespace logic relies on the javascript template evaluation to be left to right
if (options === undefined) { if (options === undefined) {
options = { options = {
truncateTo: undefined,
collapseWhitespace: false,
padDirection: 'left', padDirection: 'left',
collapseWhitespace: false
prefix: undefined,
suffix: undefined,
truncateTo: undefined
}; };
} }
let max = options.truncateTo; let max = options.truncateTo;
if (max === undefined) { if (max === undefined) {
if (this.collapsableWhitespace === 0) return s;
if (this.collapsableWhitespace !== 0) {
const width = Strings.getWidth(s);
const width = Strings.getWidth(s);
// If we have left over whitespace make sure it gets re-added
const diff = this.collapsableWhitespace - width;
this.collapsableWhitespace = 0;
// If we have left over whitespace make sure it gets re-added
const diff = this.collapsableWhitespace - width;
this.collapsableWhitespace = 0;
if (diff <= 0) return s;
if (options.truncateTo === undefined) return s;
return Strings.padLeft(s, diff, undefined, width);
if (diff > 0 && options.truncateTo !== undefined) {
s = Strings.padLeft(s, diff, undefined, width);
}
}
} }
else {
max += this.collapsableWhitespace;
this.collapsableWhitespace = 0;
max += this.collapsableWhitespace;
this.collapsableWhitespace = 0;
const width = Strings.getWidth(s);
const diff = max - width;
if (diff > 0) {
if (options.collapseWhitespace) {
this.collapsableWhitespace = diff;
const width = Strings.getWidth(s);
const diff = max - width;
if (diff > 0) {
if (options.collapseWhitespace) {
this.collapsableWhitespace = diff;
}
if (options.padDirection === 'left') {
s = Strings.padLeft(s, max, undefined, width);
}
else {
if (options.collapseWhitespace) {
max -= diff;
}
s = Strings.padRight(s, max, undefined, width);
}
} }
if (options.padDirection === 'left') return Strings.padLeft(s, max, undefined, width);
if (options.collapseWhitespace) {
max -= diff;
else if (diff < 0) {
s = Strings.truncate(s, max, undefined, width);
} }
return Strings.padRight(s, max, undefined, width);
} }
if (diff < 0) return Strings.truncate(s, max, undefined, width);
if (options.prefix || options.suffix) {
s = `${options.prefix || ''}${s}${options.suffix || ''}`;
}
return s; return s;
} }
@ -107,6 +119,15 @@ export abstract class Formatter
let options: TOptions | undefined = undefined; let options: TOptions | undefined = undefined;
if (dateFormatOrOptions == null || typeof dateFormatOrOptions === 'string') { if (dateFormatOrOptions == null || typeof dateFormatOrOptions === 'string') {
options = {
dateFormat: dateFormatOrOptions
} as TOptions;
}
else {
options = dateFormatOrOptions;
}
if (options.tokenOptions == null) {
const tokenOptions = Strings.getTokensFromTemplate(template).reduce( const tokenOptions = Strings.getTokensFromTemplate(template).reduce(
(map, token) => { (map, token) => {
map[token.key] = token.options; map[token.key] = token.options;
@ -115,13 +136,7 @@ export abstract class Formatter
{} as { [token: string]: Strings.ITokenOptions | undefined } {} as { [token: string]: Strings.ITokenOptions | undefined }
); );
options = {
dateFormat: dateFormatOrOptions,
tokenOptions: tokenOptions
} as TOptions;
}
else {
options = dateFormatOrOptions;
options.tokenOptions = tokenOptions;
} }
if (this._formatter === undefined) { if (this._formatter === undefined) {

+ 6
- 2
src/git/formatters/statusFormatter.ts View File

@ -14,13 +14,14 @@ export interface IStatusFormatOptions extends IFormatOptions {
filePath?: Strings.ITokenOptions; filePath?: Strings.ITokenOptions;
path?: Strings.ITokenOptions; path?: Strings.ITokenOptions;
status?: Strings.ITokenOptions; status?: Strings.ITokenOptions;
working?: Strings.ITokenOptions;
}; };
} }
export class StatusFileFormatter extends Formatter<IGitStatusFile, IStatusFormatOptions> { export class StatusFileFormatter extends Formatter<IGitStatusFile, IStatusFormatOptions> {
get directory() { get directory() {
const directory = GitStatusFile.getFormattedDirectory(this._item, false, this._options.relativePath); const directory = GitStatusFile.getFormattedDirectory(this._item, false, this._options.relativePath);
return this._padOrTruncate(directory, this._options.tokenOptions!.file);
return this._padOrTruncate(directory, this._options.tokenOptions!.directory);
} }
get file() { get file() {
@ -45,7 +46,10 @@ export class StatusFileFormatter extends Formatter
get working() { get working() {
const commit = (this._item as IGitStatusFileWithCommit).commit; const commit = (this._item as IGitStatusFileWithCommit).commit;
return commit !== undefined && commit.isUncommitted ? `${GlyphChars.Pencil} ${GlyphChars.Space}` : '';
return this._padOrTruncate(
commit !== undefined && commit.isUncommitted ? GlyphChars.Pencil : '',
this._options.tokenOptions!.working
);
} }
static fromTemplate(template: string, status: IGitStatusFile, dateFormat: string | null): string; static fromTemplate(template: string, status: IGitStatusFile, dateFormat: string | null): string;

+ 63
- 18
src/git/models/logCommit.ts View File

@ -59,27 +59,72 @@ export class GitLogCommit extends GitCommit {
return this.isFile && this.previousSha ? this.previousSha : `${this.sha}^`; return this.isFile && this.previousSha ? this.previousSha : `${this.sha}^`;
} }
getDiffStatus(): string {
let added = 0;
let deleted = 0;
let changed = 0;
for (const f of this.fileStatuses) {
switch (f.status) {
case 'A':
case '?':
added++;
break;
case 'D':
deleted++;
break;
default:
changed++;
break;
private _diff?: {
added: number;
deleted: number;
changed: number;
};
getDiffStatus() {
if (this._diff === undefined) {
this._diff = {
added: 0,
deleted: 0,
changed: 0
};
if (this.fileStatuses.length !== 0) {
for (const f of this.fileStatuses) {
switch (f.status) {
case 'A':
case '?':
this._diff.added++;
break;
case 'D':
this._diff.deleted++;
break;
default:
this._diff.changed++;
break;
}
}
}
}
return this._diff;
}
getFormattedDiffStatus(
options: {
compact?: boolean;
empty?: string;
expand?: boolean;
prefix?: string;
separator?: string;
suffix?: string;
} = {}
): string {
const { added, changed, deleted } = this.getDiffStatus();
if (added === 0 && changed === 0 && deleted === 0) return options.empty || '';
options = { compact: true, empty: '', prefix: '', separator: ' ', suffix: '', ...options };
if (options.expand) {
let status = '';
if (added) {
status += `${Strings.pluralize('file', added)} added`;
}
if (changed) {
status += `${status === '' ? '' : options.separator}${Strings.pluralize('file', changed)} changed`;
}
if (deleted) {
status += `${status === '' ? '' : options.separator}${Strings.pluralize('file', deleted)} deleted`;
} }
return `${options.prefix}${status}${options.suffix}`;
} }
return `+${added} ~${changed} -${deleted}`;
return `${options.prefix}${options.compact && added === 0 ? '' : `+${added}${options.separator}`}${
options.compact && changed === 0 ? '' : `~${changed}${options.separator}`
}${options.compact && deleted === 0 ? '' : `-${deleted}`}${options.suffix}`;
} }
toFileCommit(fileName: string): GitLogCommit | undefined; toFileCommit(fileName: string): GitLogCommit | undefined;

+ 42
- 32
src/git/models/status.ts View File

@ -39,10 +39,7 @@ export class GitStatus {
changed: number; changed: number;
}; };
getDiffStatus(options: { empty?: string; expand?: boolean; prefix?: string; separator?: string } = {}): string {
options = { empty: '', prefix: '', separator: ' ', ...options };
if (this.files.length === 0) return options.empty!;
getDiffStatus() {
if (this._diff === undefined) { if (this._diff === undefined) {
this._diff = { this._diff = {
added: 0, added: 0,
@ -50,45 +47,58 @@ export class GitStatus {
changed: 0 changed: 0
}; };
for (const f of this.files) {
switch (f.status) {
case 'A':
case '?':
this._diff.added++;
break;
case 'D':
this._diff.deleted++;
break;
default:
this._diff.changed++;
break;
if (this.files.length !== 0) {
for (const f of this.files) {
switch (f.status) {
case 'A':
case '?':
this._diff.added++;
break;
case 'D':
this._diff.deleted++;
break;
default:
this._diff.changed++;
break;
}
} }
} }
} }
return this._diff;
}
getFormattedDiffStatus(
options: {
compact?: boolean;
empty?: string;
expand?: boolean;
prefix?: string;
separator?: string;
suffix?: string;
} = {}
): string {
const { added, changed, deleted } = this.getDiffStatus();
if (added === 0 && changed === 0 && deleted === 0) return options.empty || '';
options = { compact: true, empty: '', prefix: '', separator: ' ', suffix: '', ...options };
if (options.expand) { if (options.expand) {
let status = ''; let status = '';
if (this._diff.added) {
status += `${Strings.pluralize('file', this._diff.added)} added`;
if (added) {
status += `${Strings.pluralize('file', added)} added`;
} }
if (this._diff.changed) {
status += `${status === '' ? '' : options.separator}${this._diff.changed} ${Strings.pluralize(
'file',
this._diff.changed
)} changed`;
if (changed) {
status += `${status === '' ? '' : options.separator}${Strings.pluralize('file', changed)} changed`;
} }
if (this._diff.deleted) {
status += `${status === '' ? '' : options.separator}${this._diff.deleted} ${Strings.pluralize(
'file',
this._diff.deleted
)} deleted`;
if (deleted) {
status += `${status === '' ? '' : options.separator}${Strings.pluralize('file', deleted)} deleted`;
} }
return `${options.prefix}${status}`;
return `${options.prefix}${status}${options.suffix}`;
} }
return `${options.prefix}+${this._diff.added}${options.separator}~${this._diff.changed}${options.separator}-${
this._diff.deleted
}`;
return `${options.prefix}${options.compact && added === 0 ? '' : `+${added}${options.separator}`}${
options.compact && changed === 0 ? '' : `~${changed}${options.separator}`
}${options.compact && deleted === 0 ? '' : `-${deleted}`}${options.suffix}`;
} }
getUpstreamStatus(options: { empty?: string; expand?: boolean; prefix?: string; separator?: string }): string { getUpstreamStatus(options: { empty?: string; expand?: boolean; prefix?: string; separator?: string }): string {

+ 1
- 1
src/quickpicks/commitQuickPick.ts View File

@ -276,7 +276,7 @@ export class CommitQuickPick {
new CommandQuickPickItem( new CommandQuickPickItem(
{ {
label: `Changed Files`, label: `Changed Files`,
description: commit.getDiffStatus()
description: commit.getFormattedDiffStatus()
}, },
Commands.ShowQuickCommitDetails, Commands.ShowQuickCommitDetails,
[ [

+ 2
- 2
src/quickpicks/commonQuickPicks.ts View File

@ -116,13 +116,13 @@ export class CommitQuickPickItem implements QuickPickItem {
GlyphChars.Dot, GlyphChars.Dot,
1, 1,
1 1
)} ${commit.formattedDate} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.getDiffStatus()}`;
)} ${commit.formattedDate} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.getFormattedDiffStatus()}`;
} }
else { else {
this.label = message; this.label = message;
this.description = `${Strings.pad('$(git-commit)', 1, 1)} ${commit.shortSha}`; this.description = `${Strings.pad('$(git-commit)', 1, 1)} ${commit.shortSha}`;
this.detail = `${GlyphChars.Space} ${commit.author}, ${commit.formattedDate}${ this.detail = `${GlyphChars.Space} ${commit.author}, ${commit.formattedDate}${
commit.isFile ? '' : ` ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.getDiffStatus()}`
commit.isFile ? '' : ` ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.getFormattedDiffStatus()}`
}`; }`;
} }
} }

+ 11
- 8
src/system/string.ts View File

@ -8,13 +8,15 @@ export namespace Strings {
} }
const pathNormalizer = /\\/g; const pathNormalizer = /\\/g;
const TokenRegex = /\$\{([^|]*?)(?:\|(\d+)(\-|\?)?)?\}/g;
const TokenSanitizeRegex = /\$\{(\w*?)(?:\W|\d)*?\}/g;
const TokenRegex = /\$\{(\W*)?([^|]*?)(?:\|(\d+)(\-|\?)?)?(\W*)?\}/g;
const TokenSanitizeRegex = /\$\{(?:\W*)?(\w*?)(?:[\W\d]*)\}/g;
export interface ITokenOptions { export interface ITokenOptions {
collapseWhitespace: boolean;
padDirection: 'left' | 'right'; padDirection: 'left' | 'right';
prefix: string | undefined;
suffix: string | undefined;
truncateTo: number | undefined; truncateTo: number | undefined;
collapseWhitespace: boolean;
} }
export function getTokensFromTemplate(template: string) { export function getTokensFromTemplate(template: string) {
@ -22,14 +24,15 @@ export namespace Strings {
let match = TokenRegex.exec(template); let match = TokenRegex.exec(template);
while (match != null) { while (match != null) {
const truncateTo = match[2];
const option = match[3];
const [, prefix, key, truncateTo, option, suffix] = match;
tokens.push({ tokens.push({
key: match[1],
key: key,
options: { options: {
truncateTo: truncateTo == null ? undefined : parseInt(truncateTo, 10),
collapseWhitespace: option === '?',
padDirection: option === '-' ? 'left' : 'right', padDirection: option === '-' ? 'left' : 'right',
collapseWhitespace: option === '?'
prefix: prefix,
suffix: suffix,
truncateTo: truncateTo == null ? undefined : parseInt(truncateTo, 10)
} }
}); });
match = TokenRegex.exec(template); match = TokenRegex.exec(template);

+ 8
- 0
src/views/nodes/commitNode.ts View File

@ -91,6 +91,14 @@ export class CommitNode extends ExplorerRefNode {
} as ICommitFormatOptions } as ICommitFormatOptions
); );
if (!this.commit.isUncommitted) {
item.tooltip += this.commit.getFormattedDiffStatus({
expand: true,
prefix: '\n\n',
separator: '\n'
});
}
return item; return item;
} }

+ 2
- 2
src/views/nodes/statusNode.ts View File

@ -87,7 +87,7 @@ export class StatusNode extends ExplorerNode {
let hasChildren = false; let hasChildren = false;
const hasWorkingChanges = status.files.length !== 0 && this.includeWorkingTree; const hasWorkingChanges = status.files.length !== 0 && this.includeWorkingTree;
let label = `${status.getUpstreamStatus({ prefix: `${GlyphChars.Space} ` })}${ let label = `${status.getUpstreamStatus({ prefix: `${GlyphChars.Space} ` })}${
hasWorkingChanges ? status.getDiffStatus({ prefix: `${GlyphChars.Space} ` }) : ''
hasWorkingChanges ? status.getFormattedDiffStatus({ prefix: `${GlyphChars.Space} ` }) : ''
}`; }`;
let tooltip = `${status.branch} (current)`; let tooltip = `${status.branch} (current)`;
let iconSuffix = ''; let iconSuffix = '';
@ -112,7 +112,7 @@ ${status.getUpstreamStatus({ empty: 'up-to-date', expand: true, separator: '\n'
} }
if (hasWorkingChanges) { if (hasWorkingChanges) {
tooltip += `\n\nHas uncommitted changes${status.getDiffStatus({
tooltip += `\n\nHas uncommitted changes${status.getFormattedDiffStatus({
expand: true, expand: true,
prefix: `\n`, prefix: `\n`,
separator: '\n' separator: '\n'

Loading…
Cancel
Save