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:
Ladislau Szomoru 2023-11-13 16:49:28 +01:00 committed by GitHub
parent a87d034b0d
commit e447d54e1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 259 additions and 40 deletions

View file

@ -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 {

View file

@ -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[]> {

View file

@ -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
};
}));

View file

@ -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
}
}]);
});
});

View file

@ -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}`);
}