Skip to content

Commit b9112e6

Browse files
committed
mhutchie#498 Verify and display the signature status of signed tags on the "View Details" Dialog.
1 parent 7eab373 commit b9112e6

File tree

6 files changed

+417
-69
lines changed

6 files changed

+417
-69
lines changed

src/dataSource.ts

Lines changed: 101 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as vscode from 'vscode';
66
import { AskpassEnvironment, AskpassManager } from './askpass/askpassManager';
77
import { getConfig } from './config';
88
import { Logger } from './logger';
9-
import { CommitOrdering, DateType, DeepWriteable, ErrorInfo, GitCommit, GitCommitDetails, GitCommitStash, GitConfigLocation, GitFileChange, GitFileStatus, GitPushBranchMode, GitRepoConfig, GitRepoConfigBranches, GitResetMode, GitSignatureStatus, GitStash, MergeActionOn, RebaseActionOn, SquashMessageFormat, TagType, Writeable } from './types';
9+
import { CommitOrdering, DateType, DeepWriteable, ErrorInfo, GitCommit, GitCommitDetails, GitCommitStash, GitConfigLocation, GitFileChange, GitFileStatus, GitPushBranchMode, GitRepoConfig, GitRepoConfigBranches, GitResetMode, GitSignature, GitSignatureStatus, GitStash, GitTagDetails, MergeActionOn, RebaseActionOn, SquashMessageFormat, TagType, Writeable } from './types';
1010
import { GitExecutable, UNABLE_TO_FIND_GIT_MSG, UNCOMMITTED, abbrevCommit, constructIncompatibleGitVersionMessage, doesVersionMeetRequirement, getPathFromStr, getPathFromUri, openGitTerminal, pathWithTrailingSlash, realpath, resolveSpawnOutput, showErrorMessage } from './utils';
1111
import { Disposable } from './utils/disposable';
1212
import { Event } from './utils/event';
@@ -31,6 +31,15 @@ export const GIT_CONFIG = {
3131
}
3232
};
3333

34+
const GPG_STATUS_CODE_PARSING_DETAILS: { [statusCode: string]: GpgStatusCodeParsingDetails } = {
35+
'GOODSIG': { status: GitSignatureStatus.GoodAndValid, uid: true },
36+
'BADSIG': { status: GitSignatureStatus.Bad, uid: true },
37+
'ERRSIG': { status: GitSignatureStatus.CannotBeChecked, uid: false },
38+
'EXPSIG': { status: GitSignatureStatus.GoodButExpired, uid: true },
39+
'EXPKEYSIG': { status: GitSignatureStatus.GoodButMadeByExpiredKey, uid: true },
40+
'REVKEYSIG': { status: GitSignatureStatus.GoodButMadeByRevokedKey, uid: true }
41+
};
42+
3443
/**
3544
* Interfaces Git Graph with the Git executable to provide all Git integrations.
3645
*/
@@ -494,21 +503,37 @@ export class DataSource extends Disposable {
494503
* @returns The tag details.
495504
*/
496505
public getTagDetails(repo: string, tagName: string): Promise<GitTagDetailsData> {
497-
return this.spawnGit(['for-each-ref', 'refs/tags/' + tagName, '--format=' + ['%(objectname)', '%(taggername)', '%(taggeremail)', '%(taggerdate:unix)', '%(contents)'].join(GIT_LOG_SEPARATOR)], repo, (stdout) => {
498-
let data = stdout.split(GIT_LOG_SEPARATOR);
506+
if (this.gitExecutable !== null && !doesVersionMeetRequirement(this.gitExecutable.version, '1.7.8')) {
507+
return Promise.resolve({ details: null, error: constructIncompatibleGitVersionMessage(this.gitExecutable, '1.7.8', 'retrieving Tag Details') });
508+
}
509+
510+
const ref = 'refs/tags/' + tagName;
511+
return this.spawnGit(['for-each-ref', ref, '--format=' + ['%(objectname)', '%(taggername)', '%(taggeremail)', '%(taggerdate:unix)', '%(contents:signature)', '%(contents)'].join(GIT_LOG_SEPARATOR)], repo, (stdout) => {
512+
const data = stdout.split(GIT_LOG_SEPARATOR);
499513
return {
500-
tagHash: data[0],
501-
name: data[1],
502-
email: data[2].substring(data[2].startsWith('<') ? 1 : 0, data[2].length - (data[2].endsWith('>') ? 1 : 0)),
503-
date: parseInt(data[3]),
504-
message: removeTrailingBlankLines(data[4].split(EOL_REGEX)).join('\n'),
505-
error: null
514+
hash: data[0],
515+
taggerName: data[1],
516+
taggerEmail: data[2].substring(data[2].startsWith('<') ? 1 : 0, data[2].length - (data[2].endsWith('>') ? 1 : 0)),
517+
taggerDate: parseInt(data[3]),
518+
message: removeTrailingBlankLines(data.slice(5).join(GIT_LOG_SEPARATOR).replace(data[4], '').split(EOL_REGEX)).join('\n'),
519+
signed: data[4] !== ''
506520
};
507-
}).then((data) => {
508-
return data;
509-
}).catch((errorMessage) => {
510-
return { tagHash: '', name: '', email: '', date: 0, message: '', error: errorMessage };
511-
});
521+
}).then(async (tag) => ({
522+
details: {
523+
hash: tag.hash,
524+
taggerName: tag.taggerName,
525+
taggerEmail: tag.taggerEmail,
526+
taggerDate: tag.taggerDate,
527+
message: tag.message,
528+
signature: tag.signed
529+
? await this.getTagSignature(repo, ref)
530+
: null
531+
},
532+
error: null
533+
})).catch((errorMessage) => ({
534+
details: null,
535+
error: errorMessage
536+
}));
512537
}
513538

514539
/**
@@ -1595,6 +1620,52 @@ export class DataSource extends Disposable {
15951620
});
15961621
}
15971622

1623+
/**
1624+
* Get the signature of a signed tag.
1625+
* @param repo The path of the repository.
1626+
* @param ref The reference identifying the tag.
1627+
* @returns A Promise resolving to the signature.
1628+
*/
1629+
private getTagSignature(repo: string, ref: string): Promise<GitSignature> {
1630+
return this._spawnGit(['verify-tag', '--raw', ref], repo, (stdout, stderr) => stderr || stdout.toString(), true).then((output) => {
1631+
const records = output.split(EOL_REGEX)
1632+
.filter((line) => line.startsWith('[GNUPG:] '))
1633+
.map((line) => line.split(' '));
1634+
1635+
let signature: Writeable<GitSignature> | null = null, trustLevel: string | null = null, parsingDetails: GpgStatusCodeParsingDetails | undefined;
1636+
for (let i = 0; i < records.length; i++) {
1637+
parsingDetails = GPG_STATUS_CODE_PARSING_DETAILS[records[i][1]];
1638+
if (parsingDetails) {
1639+
if (signature !== null) {
1640+
throw new Error('Multiple Signatures Exist: As Git currently doesn\'t support them, nor does Git Graph (for consistency).');
1641+
} else {
1642+
signature = {
1643+
status: parsingDetails.status,
1644+
key: records[i][2],
1645+
signer: parsingDetails.uid ? records[i].slice(3).join(' ') : '' // When parsingDetails.uid === TRUE, the signer is the rest of the record (so join the remaining arguments)
1646+
};
1647+
}
1648+
} else if (records[i][1].startsWith('TRUST_')) {
1649+
trustLevel = records[i][1];
1650+
}
1651+
}
1652+
1653+
if (signature !== null && signature.status === GitSignatureStatus.GoodAndValid && (trustLevel === 'TRUST_UNDEFINED' || trustLevel === 'TRUST_NEVER')) {
1654+
signature.status = GitSignatureStatus.GoodWithUnknownValidity;
1655+
}
1656+
1657+
if (signature !== null) {
1658+
return signature;
1659+
} else {
1660+
throw new Error('No Signature could be parsed.');
1661+
}
1662+
}).catch(() => ({
1663+
status: GitSignatureStatus.CannotBeChecked,
1664+
key: '',
1665+
signer: ''
1666+
}));
1667+
}
1668+
15981669
/**
15991670
* Get the number of uncommitted changes in a repository.
16001671
* @param repo The path of the repository.
@@ -1715,21 +1786,24 @@ export class DataSource extends Disposable {
17151786
* Spawn Git, with the return value resolved from `stdout` as a buffer.
17161787
* @param args The arguments to pass to Git.
17171788
* @param repo The repository to run the command in.
1718-
* @param resolveValue A callback invoked to resolve the data from `stdout`.
1789+
* @param resolveValue A callback invoked to resolve the data from `stdout` and `stderr`.
1790+
* @param ignoreExitCode Ignore the exit code returned by Git (default: `FALSE`).
17191791
*/
1720-
private _spawnGit<T>(args: string[], repo: string, resolveValue: { (stdout: Buffer): T }) {
1792+
private _spawnGit<T>(args: string[], repo: string, resolveValue: { (stdout: Buffer, stderr: string): T }, ignoreExitCode: boolean = false) {
17211793
return new Promise<T>((resolve, reject) => {
1722-
if (this.gitExecutable === null) return reject(UNABLE_TO_FIND_GIT_MSG);
1794+
if (this.gitExecutable === null) {
1795+
return reject(UNABLE_TO_FIND_GIT_MSG);
1796+
}
17231797

17241798
resolveSpawnOutput(cp.spawn(this.gitExecutable.path, args, {
17251799
cwd: repo,
17261800
env: Object.assign({}, process.env, this.askpassEnv)
17271801
})).then((values) => {
1728-
let status = values[0], stdout = values[1];
1729-
if (status.code === 0) {
1730-
resolve(resolveValue(stdout));
1802+
const status = values[0], stdout = values[1], stderr = values[2];
1803+
if (status.code === 0 || ignoreExitCode) {
1804+
resolve(resolveValue(stdout, stderr));
17311805
} else {
1732-
reject(getErrorMessage(status.error, stdout, values[2]));
1806+
reject(getErrorMessage(status.error, stdout, stderr));
17331807
}
17341808
});
17351809

@@ -1915,10 +1989,11 @@ interface GitStatusFiles {
19151989
}
19161990

19171991
interface GitTagDetailsData {
1918-
tagHash: string;
1919-
name: string;
1920-
email: string;
1921-
date: number;
1922-
message: string;
1992+
details: GitTagDetails | null;
19231993
error: ErrorInfo;
19241994
}
1995+
1996+
interface GpgStatusCodeParsingDetails {
1997+
status: GitSignatureStatus,
1998+
uid: boolean
1999+
}

src/types.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export interface GitCommitDetails {
3838
readonly committer: string;
3939
readonly committerEmail: string;
4040
readonly committerDate: number;
41-
readonly signature: GitCommitSignature | null;
41+
readonly signature: GitSignature | null;
4242
readonly body: string;
4343
readonly fileChanges: ReadonlyArray<GitFileChange>;
4444
}
@@ -53,7 +53,7 @@ export const enum GitSignatureStatus {
5353
Bad = 'B'
5454
}
5555

56-
export interface GitCommitSignature {
56+
export interface GitSignature {
5757
readonly key: string;
5858
readonly signer: string;
5959
readonly status: GitSignatureStatus;
@@ -135,6 +135,15 @@ export interface GitStash {
135135
readonly message: string;
136136
}
137137

138+
export interface GitTagDetails {
139+
readonly hash: string;
140+
readonly taggerName: string;
141+
readonly taggerEmail: string;
142+
readonly taggerDate: number;
143+
readonly message: string;
144+
readonly signature: GitSignature | null;
145+
}
146+
138147

139148
/* Git Repo State */
140149

@@ -1149,12 +1158,8 @@ export interface RequestTagDetails extends RepoRequest {
11491158
export interface ResponseTagDetails extends ResponseWithErrorInfo {
11501159
readonly command: 'tagDetails';
11511160
readonly tagName: string;
1152-
readonly tagHash: string;
11531161
readonly commitHash: string;
1154-
readonly name: string;
1155-
readonly email: string;
1156-
readonly date: number;
1157-
readonly message: string;
1162+
readonly details: GitTagDetails | null;
11581163
}
11591164

11601165
export interface RequestUpdateCodeReview extends RepoRequest {

0 commit comments

Comments
 (0)