mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 04:49:35 +00:00
SCM - Add short state badge for history items (#198126)
* Git - Extract parsing of git diff short stat * Add shortStat badge for commits * Git - specify diff-merges for merge commits * Add tests
This commit is contained in:
parent
a87d034b0d
commit
e447d54e1e
8
extensions/git/src/api/git.d.ts
vendored
8
extensions/git/src/api/git.d.ts
vendored
|
@ -44,6 +44,12 @@ export interface Branch extends Ref {
|
|||
readonly behind?: number;
|
||||
}
|
||||
|
||||
export interface CommitShortStat {
|
||||
readonly files: number;
|
||||
readonly insertions: number;
|
||||
readonly deletions: number;
|
||||
}
|
||||
|
||||
export interface Commit {
|
||||
readonly hash: string;
|
||||
readonly message: string;
|
||||
|
@ -52,6 +58,7 @@ export interface Commit {
|
|||
readonly authorName?: string;
|
||||
readonly authorEmail?: string;
|
||||
readonly commitDate?: Date;
|
||||
readonly shortStat?: CommitShortStat;
|
||||
}
|
||||
|
||||
export interface Submodule {
|
||||
|
@ -134,6 +141,7 @@ export interface LogOptions {
|
|||
readonly range?: string;
|
||||
readonly reverse?: boolean;
|
||||
readonly sortByAuthorDate?: boolean;
|
||||
readonly shortStats?: boolean;
|
||||
}
|
||||
|
||||
export interface CommitOptions {
|
||||
|
|
|
@ -706,6 +706,12 @@ export class Git {
|
|||
}
|
||||
}
|
||||
|
||||
export interface CommitShortStat {
|
||||
readonly files: number;
|
||||
readonly insertions: number;
|
||||
readonly deletions: number;
|
||||
}
|
||||
|
||||
export interface Commit {
|
||||
hash: string;
|
||||
message: string;
|
||||
|
@ -715,6 +721,7 @@ export interface Commit {
|
|||
authorEmail?: string;
|
||||
commitDate?: Date;
|
||||
refNames: string[];
|
||||
shortStat?: CommitShortStat;
|
||||
}
|
||||
|
||||
interface GitConfigSection {
|
||||
|
@ -866,7 +873,7 @@ export function parseGitRemotes(raw: string): MutableRemote[] {
|
|||
return remotes;
|
||||
}
|
||||
|
||||
const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm;
|
||||
const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)(?:\n((?:.*)files? changed(?:.*))$)?/gm;
|
||||
|
||||
export function parseGitCommits(data: string): Commit[] {
|
||||
const commits: Commit[] = [];
|
||||
|
@ -879,6 +886,7 @@ export function parseGitCommits(data: string): Commit[] {
|
|||
let parents;
|
||||
let refNames;
|
||||
let message;
|
||||
let shortStat;
|
||||
let match;
|
||||
|
||||
do {
|
||||
|
@ -887,7 +895,7 @@ export function parseGitCommits(data: string): Commit[] {
|
|||
break;
|
||||
}
|
||||
|
||||
[, ref, authorName, authorEmail, authorDate, commitDate, parents, refNames, message] = match;
|
||||
[, ref, authorName, authorEmail, authorDate, commitDate, parents, refNames, message, shortStat] = match;
|
||||
|
||||
if (message[message.length - 1] === '\n') {
|
||||
message = message.substr(0, message.length - 1);
|
||||
|
@ -902,13 +910,27 @@ export function parseGitCommits(data: string): Commit[] {
|
|||
authorName: ` ${authorName}`.substr(1),
|
||||
authorEmail: ` ${authorEmail}`.substr(1),
|
||||
commitDate: new Date(Number(commitDate) * 1000),
|
||||
refNames: refNames.split(',').map(s => s.trim())
|
||||
refNames: refNames.split(',').map(s => s.trim()),
|
||||
shortStat: shortStat ? parseGitDiffShortStat(shortStat) : undefined
|
||||
});
|
||||
} while (true);
|
||||
|
||||
return commits;
|
||||
}
|
||||
|
||||
const diffShortStatRegex = /(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/;
|
||||
|
||||
function parseGitDiffShortStat(data: string): CommitShortStat {
|
||||
const matches = data.trim().match(diffShortStatRegex);
|
||||
|
||||
if (!matches) {
|
||||
return { files: 0, insertions: 0, deletions: 0 };
|
||||
}
|
||||
|
||||
const [, files, insertions = undefined, deletions = undefined] = matches;
|
||||
return { files: parseInt(files), insertions: parseInt(insertions ?? '0'), deletions: parseInt(deletions ?? '0') };
|
||||
}
|
||||
|
||||
interface LsTreeElement {
|
||||
mode: string;
|
||||
type: string;
|
||||
|
@ -1018,6 +1040,10 @@ export class Repository {
|
|||
async log(options?: LogOptions): Promise<Commit[]> {
|
||||
const args = ['log', `--format=${COMMIT_FORMAT}`, '-z'];
|
||||
|
||||
if (options?.shortStats) {
|
||||
args.push('--shortstat', '--diff-merges=first-parent');
|
||||
}
|
||||
|
||||
if (options?.reverse) {
|
||||
args.push('--reverse', '--ancestry-path');
|
||||
}
|
||||
|
@ -1324,15 +1350,7 @@ export class Repository {
|
|||
return { files: 0, insertions: 0, deletions: 0 };
|
||||
}
|
||||
|
||||
const regex = /(\d+) files? changed(?:, (\d+) insertions\(\+\))?(?:, (\d+) deletions\(-\))?/;
|
||||
const matches = result.stdout.trim().match(regex);
|
||||
|
||||
if (!matches) {
|
||||
return { files: 0, insertions: 0, deletions: 0 };
|
||||
}
|
||||
|
||||
const [, files, insertions = undefined, deletions = undefined] = matches;
|
||||
return { files: parseInt(files), insertions: parseInt(insertions ?? '0'), deletions: parseInt(deletions ?? '0') };
|
||||
return parseGitDiffShortStat(result.stdout.trim());
|
||||
}
|
||||
|
||||
private async diffFiles(cached: boolean, ref?: string): Promise<Change[]> {
|
||||
|
|
|
@ -80,7 +80,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
|||
const historyItemGroupIdRef = await this.repository.revParse(historyItemGroupId) ?? '';
|
||||
|
||||
const [commits, summary] = await Promise.all([
|
||||
this.repository.log({ range: `${optionsRef}..${historyItemGroupIdRef}`, sortByAuthorDate: true }),
|
||||
this.repository.log({ range: `${optionsRef}..${historyItemGroupIdRef}`, shortStats: true, sortByAuthorDate: true }),
|
||||
this.getSummaryHistoryItem(optionsRef, historyItemGroupIdRef)
|
||||
]);
|
||||
|
||||
|
@ -97,7 +97,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
|||
label: emojify(subject),
|
||||
description: commit.authorName,
|
||||
icon: new ThemeIcon('git-commit'),
|
||||
timestamp: commit.authorDate?.getTime()
|
||||
timestamp: commit.authorDate?.getTime(),
|
||||
statistics: commit.shortStat
|
||||
};
|
||||
}));
|
||||
|
||||
|
|
|
@ -270,14 +270,15 @@ suite('git', () => {
|
|||
|
||||
suite('parseGitCommit', () => {
|
||||
test('single parent commit', function () {
|
||||
const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1
|
||||
John Doe
|
||||
john.doe@mail.com
|
||||
1580811030
|
||||
1580811031
|
||||
8e5a374372b8393906c7e380dbb09349c5385554
|
||||
main,branch
|
||||
This is a commit message.\x00`;
|
||||
const GIT_OUTPUT_SINGLE_PARENT =
|
||||
'52c293a05038d865604c2284aa8698bd087915a1\n' +
|
||||
'John Doe\n' +
|
||||
'john.doe@mail.com\n' +
|
||||
'1580811030\n' +
|
||||
'1580811031\n' +
|
||||
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
|
||||
'main,branch\n' +
|
||||
'This is a commit message.\x00';
|
||||
|
||||
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
|
@ -288,18 +289,20 @@ This is a commit message.\x00`;
|
|||
authorEmail: 'john.doe@mail.com',
|
||||
commitDate: new Date(1580811031000),
|
||||
refNames: ['main', 'branch'],
|
||||
shortStat: undefined
|
||||
}]);
|
||||
});
|
||||
|
||||
test('multiple parent commits', function () {
|
||||
const GIT_OUTPUT_MULTIPLE_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
|
||||
John Doe
|
||||
john.doe@mail.com
|
||||
1580811030
|
||||
1580811031
|
||||
8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217
|
||||
main
|
||||
This is a commit message.\x00`;
|
||||
const GIT_OUTPUT_MULTIPLE_PARENTS =
|
||||
'52c293a05038d865604c2284aa8698bd087915a1\n' +
|
||||
'John Doe\n' +
|
||||
'john.doe@mail.com\n' +
|
||||
'1580811030\n' +
|
||||
'1580811031\n' +
|
||||
'8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217\n' +
|
||||
'main\n' +
|
||||
'This is a commit message.\x00';
|
||||
|
||||
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
|
@ -310,18 +313,20 @@ This is a commit message.\x00`;
|
|||
authorEmail: 'john.doe@mail.com',
|
||||
commitDate: new Date(1580811031000),
|
||||
refNames: ['main'],
|
||||
shortStat: undefined
|
||||
}]);
|
||||
});
|
||||
|
||||
test('no parent commits', function () {
|
||||
const GIT_OUTPUT_NO_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
|
||||
John Doe
|
||||
john.doe@mail.com
|
||||
1580811030
|
||||
1580811031
|
||||
|
||||
main
|
||||
This is a commit message.\x00`;
|
||||
const GIT_OUTPUT_NO_PARENTS =
|
||||
'52c293a05038d865604c2284aa8698bd087915a1\n' +
|
||||
'John Doe\n' +
|
||||
'john.doe@mail.com\n' +
|
||||
'1580811030\n' +
|
||||
'1580811031\n' +
|
||||
'\n' +
|
||||
'main\n' +
|
||||
'This is a commit message.\x00';
|
||||
|
||||
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
|
@ -332,6 +337,191 @@ This is a commit message.\x00`;
|
|||
authorEmail: 'john.doe@mail.com',
|
||||
commitDate: new Date(1580811031000),
|
||||
refNames: ['main'],
|
||||
shortStat: undefined
|
||||
}]);
|
||||
});
|
||||
|
||||
test('commit with shortstat', function () {
|
||||
const GIT_OUTPUT_SINGLE_PARENT =
|
||||
'52c293a05038d865604c2284aa8698bd087915a1\n' +
|
||||
'John Doe\n' +
|
||||
'john.doe@mail.com\n' +
|
||||
'1580811030\n' +
|
||||
'1580811031\n' +
|
||||
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
|
||||
'main,branch\n' +
|
||||
'This is a commit message.\x00\n' +
|
||||
' 1 file changed, 2 insertions(+), 3 deletion(-)';
|
||||
|
||||
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
|
||||
authorDate: new Date(1580811030000),
|
||||
authorName: 'John Doe',
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
commitDate: new Date(1580811031000),
|
||||
refNames: ['main', 'branch'],
|
||||
shortStat: {
|
||||
deletions: 3,
|
||||
files: 1,
|
||||
insertions: 2
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
||||
test('commit with shortstat (no insertions)', function () {
|
||||
const GIT_OUTPUT_SINGLE_PARENT =
|
||||
'52c293a05038d865604c2284aa8698bd087915a1\n' +
|
||||
'John Doe\n' +
|
||||
'john.doe@mail.com\n' +
|
||||
'1580811030\n' +
|
||||
'1580811031\n' +
|
||||
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
|
||||
'main,branch\n' +
|
||||
'This is a commit message.\x00\n' +
|
||||
' 1 file changed, 3 deletion(-)';
|
||||
|
||||
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
|
||||
authorDate: new Date(1580811030000),
|
||||
authorName: 'John Doe',
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
commitDate: new Date(1580811031000),
|
||||
refNames: ['main', 'branch'],
|
||||
shortStat: {
|
||||
deletions: 3,
|
||||
files: 1,
|
||||
insertions: 0
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
||||
test('commit with shortstat (no deletions)', function () {
|
||||
const GIT_OUTPUT_SINGLE_PARENT =
|
||||
'52c293a05038d865604c2284aa8698bd087915a1\n' +
|
||||
'John Doe\n' +
|
||||
'john.doe@mail.com\n' +
|
||||
'1580811030\n' +
|
||||
'1580811031\n' +
|
||||
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
|
||||
'main,branch\n' +
|
||||
'This is a commit message.\x00\n' +
|
||||
' 1 file changed, 2 insertions(+)';
|
||||
|
||||
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
|
||||
authorDate: new Date(1580811030000),
|
||||
authorName: 'John Doe',
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
commitDate: new Date(1580811031000),
|
||||
refNames: ['main', 'branch'],
|
||||
shortStat: {
|
||||
deletions: 0,
|
||||
files: 1,
|
||||
insertions: 2
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
||||
test('commit list', function () {
|
||||
const GIT_OUTPUT_SINGLE_PARENT =
|
||||
'52c293a05038d865604c2284aa8698bd087915a1\n' +
|
||||
'John Doe\n' +
|
||||
'john.doe@mail.com\n' +
|
||||
'1580811030\n' +
|
||||
'1580811031\n' +
|
||||
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
|
||||
'main,branch\n' +
|
||||
'This is a commit message.\x00\n' +
|
||||
'52c293a05038d865604c2284aa8698bd087915a2\n' +
|
||||
'Jane Doe\n' +
|
||||
'jane.doe@mail.com\n' +
|
||||
'1580811032\n' +
|
||||
'1580811033\n' +
|
||||
'8e5a374372b8393906c7e380dbb09349c5385555\n' +
|
||||
'main,branch\n' +
|
||||
'This is another commit message.\x00';
|
||||
|
||||
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [
|
||||
{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
|
||||
authorDate: new Date(1580811030000),
|
||||
authorName: 'John Doe',
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
commitDate: new Date(1580811031000),
|
||||
refNames: ['main', 'branch'],
|
||||
shortStat: undefined,
|
||||
},
|
||||
{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a2',
|
||||
message: 'This is another commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385555'],
|
||||
authorDate: new Date(1580811032000),
|
||||
authorName: 'Jane Doe',
|
||||
authorEmail: 'jane.doe@mail.com',
|
||||
commitDate: new Date(1580811033000),
|
||||
refNames: ['main', 'branch'],
|
||||
shortStat: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('commit list with shortstat', function () {
|
||||
const GIT_OUTPUT_SINGLE_PARENT = '52c293a05038d865604c2284aa8698bd087915a1\n' +
|
||||
'John Doe\n' +
|
||||
'john.doe@mail.com\n' +
|
||||
'1580811030\n' +
|
||||
'1580811031\n' +
|
||||
'8e5a374372b8393906c7e380dbb09349c5385554\n' +
|
||||
'main,branch\n' +
|
||||
'This is a commit message.\x00\n' +
|
||||
' 5 file changed, 12 insertions(+), 13 deletion(-)\n' +
|
||||
'52c293a05038d865604c2284aa8698bd087915a2\n' +
|
||||
'Jane Doe\n' +
|
||||
'jane.doe@mail.com\n' +
|
||||
'1580811032\n' +
|
||||
'1580811033\n' +
|
||||
'8e5a374372b8393906c7e380dbb09349c5385555\n' +
|
||||
'main,branch\n' +
|
||||
'This is another commit message.\x00\n' +
|
||||
' 6 file changed, 22 insertions(+), 23 deletion(-)';
|
||||
|
||||
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
|
||||
authorDate: new Date(1580811030000),
|
||||
authorName: 'John Doe',
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
commitDate: new Date(1580811031000),
|
||||
refNames: ['main', 'branch'],
|
||||
shortStat: {
|
||||
deletions: 13,
|
||||
files: 5,
|
||||
insertions: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a2',
|
||||
message: 'This is another commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385555'],
|
||||
authorDate: new Date(1580811032000),
|
||||
authorName: 'Jane Doe',
|
||||
authorEmail: 'jane.doe@mail.com',
|
||||
commitDate: new Date(1580811033000),
|
||||
refNames: ['main', 'branch'],
|
||||
shortStat: {
|
||||
deletions: 23,
|
||||
files: 6,
|
||||
insertions: 22
|
||||
}
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -812,12 +812,14 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer<SCMHistoryItemTre
|
|||
}
|
||||
|
||||
if (historyItem.statistics?.insertions) {
|
||||
const insertionsDescription = localize('insertions', "insertions{0}", '(+)');
|
||||
const insertionsDescription = historyItem.statistics.insertions === 1 ?
|
||||
localize('insertion', "insertion{0}", '(+)') : localize('insertions', "insertions{0}", '(+)');
|
||||
statsLabelTitle.push(`${historyItem.statistics.insertions} ${insertionsDescription}`);
|
||||
}
|
||||
|
||||
if (historyItem.statistics?.deletions) {
|
||||
const deletionsDescription = localize('deletions', "deletions{0}", '(-)');
|
||||
const deletionsDescription = historyItem.statistics.deletions === 1 ?
|
||||
localize('deletion', "deletion{0}", '(-)') : localize('deletions', "deletions{0}", '(-)');
|
||||
statsLabelTitle.push(`${historyItem.statistics.deletions} ${deletionsDescription}`);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue