Browse Source

Adds assocd. PR info to line annotations & hovers

main
Eric Amodio 5 years ago
parent
commit
dca520b790
32 changed files with 1222 additions and 163 deletions
  1. +1
    -1
      .eslintrc.json
  2. +3
    -0
      images/dark/icon-help.svg
  3. +3
    -0
      images/dark/icon-plug.svg
  4. +3
    -0
      images/dark/icon-unplug.svg
  5. +57
    -3
      package.json
  6. +3
    -5
      src/annotations/annotations.ts
  7. +52
    -5
      src/annotations/lineAnnotationController.ts
  8. +1
    -0
      src/commands.ts
  9. +3
    -1
      src/commands/common.ts
  10. +97
    -0
      src/commands/remoteProviders.ts
  11. +3
    -0
      src/config.ts
  12. +14
    -1
      src/container.ts
  13. +92
    -0
      src/credentials.ts
  14. +133
    -49
      src/git/formatters/commitFormatter.ts
  15. +90
    -1
      src/git/gitService.ts
  16. +1
    -0
      src/git/models/models.ts
  17. +109
    -0
      src/git/models/pullRequest.ts
  18. +2
    -2
      src/git/models/remote.ts
  19. +32
    -7
      src/git/models/repository.ts
  20. +1
    -1
      src/git/remotes/azure-devops.ts
  21. +2
    -2
      src/git/remotes/bitbucket-server.ts
  22. +2
    -2
      src/git/remotes/bitbucket.ts
  23. +5
    -3
      src/git/remotes/factory.ts
  24. +73
    -19
      src/git/remotes/github.ts
  25. +1
    -1
      src/git/remotes/gitlab.ts
  26. +155
    -35
      src/git/remotes/provider.ts
  27. +92
    -0
      src/github/github.ts
  28. +19
    -3
      src/hovers/hovers.ts
  29. +16
    -0
      src/system/promise.ts
  30. +27
    -19
      src/views/nodes/remoteNode.ts
  31. +27
    -2
      webpack.config.js
  32. +103
    -1
      yarn.lock

+ 1
- 1
.eslintrc.json View File

@ -61,7 +61,6 @@
"no-throw-literal": "error",
"no-unmodified-loop-condition": "warn",
"no-unneeded-ternary": "error",
"no-unused-expressions": ["warn", { "allowShortCircuit": true }],
"no-use-before-define": "off",
"no-useless-call": "error",
"no-useless-catch": "error",
@ -130,6 +129,7 @@
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-unused-expressions": ["warn", { "allowShortCircuit": true }],
"@typescript-eslint/no-unused-vars": ["warn", { "args": "none" }],
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/unbound-method": "off" // Too many bugs right now: https://github.com/typescript-eslint/typescript-eslint/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+unbound-method

+ 3
- 0
images/dark/icon-help.svg View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#C5C5C5" fill-rule="evenodd" d="M3.89 2.1a6.5 6.5 0 117.182 10.835A6.5 6.5 0 013.89 2.1zm.55 10A5.5 5.5 0 109.6 2.42a5.38 5.38 0 00-3.17-.31 5.47 5.47 0 00-4.32 4.32 5.5 5.5 0 002.33 5.64v.03zM8.41 4a2.26 2.26 0 00-.85-.17 2.09 2.09 0 00-.85.17 2.33 2.33 0 00-.7.47 2.14 2.14 0 00-.46.69 1.93 1.93 0 00-.17.84h.87a1.3 1.3 0 01.8-1.21 1.3 1.3 0 01.51-.11c.179.002.356.04.52.11.315.135.565.385.7.7.064.163.098.335.1.51a1.18 1.18 0 01-.13.52 2.34 2.34 0 01-.33.43L8 7.32c-.15.14-.29.29-.42.45a2.14 2.14 0 00-.32.55 1.57 1.57 0 00-.13.68v.44H8V9a1.06 1.06 0 01.13-.52c.092-.156.203-.3.33-.43.133-.14.273-.274.42-.4.15-.14.29-.29.42-.45a2.14 2.14 0 00.32-.55A1.72 1.72 0 009.75 6a2.26 2.26 0 00-.17-.85A2.3 2.3 0 008.41 4zM8 11.22v-.88h-.87v.88H8z" clip-rule="evenodd"/>
</svg>

+ 3
- 0
images/dark/icon-plug.svg View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#C5C5C5" fill-rule="evenodd" d="M7 1H6v3H4.5l-.5.5V8a4 4 0 003.5 3.969V15h1v-3.031A4 4 0 0012 8V4.5l-.5-.5H10V1H9v3H7V1zm3.121 9.121A3 3 0 015 8V5h6v3a3 3 0 01-.879 2.121z" clip-rule="evenodd"/>
</svg>

+ 3
- 0
images/dark/icon-unplug.svg View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#C5C5C5" fill-rule="evenodd" d="M13.617 3.844a2.87 2.87 0 00-.451-.868l1.354-1.36L13.904 1l-1.36 1.354c-.264-.201-.554-.351-.868-.452a3.073 3.073 0 00-2.14.075 3.03 3.03 0 00-.991.664L7 4.192l4.327 4.328 1.552-1.545c.287-.287.508-.618.663-.992a3.074 3.074 0 00.075-2.14zm-.889 1.804a2.15 2.15 0 01-.471.705l-.93.93-3.09-3.09.93-.93a2.15 2.15 0 01.704-.472 2.134 2.134 0 011.689.007c.264.114.494.271.69.472.2.195.358.426.472.69a2.134 2.134 0 01.007 1.688zm-4.824 4.994l1.484-1.545-.616-.622-1.49 1.551-1.86-1.859 1.491-1.552L6.291 6 4.808 7.545l-.616-.615-1.551 1.545c-.287.287-.509.62-.663.998a3.023 3.023 0 00-.233 1.169c0 .332.05.656.15.97.105.31.258.597.459.862L1 13.834l.615.615 1.36-1.353c.265.2.552.353.862.458.314.1.638.15.97.15.406 0 .796-.077 1.17-.232.378-.155.71-.376.998-.663l1.545-1.552-.616-.615zm-2.262 2.023a2.16 2.16 0 01-.834.164c-.301 0-.586-.057-.855-.17a2.278 2.278 0 01-.697-.466 2.28 2.28 0 01-.465-.697 2.167 2.167 0 01-.17-.854 2.16 2.16 0 01.642-1.545l.93-.93 3.09 3.09-.93.93a2.22 2.22 0 01-.711.478z" clip-rule="evenodd"/>
</svg>

+ 57
- 3
package.json View File

@ -457,7 +457,7 @@
},
"gitlens.currentLine.format": {
"type": "string",
"default": "${author}, ${agoOrDate} • ${message}",
"default": "${author}${\" via \"pullRequest}, ${agoOrDate}${ • message}",
"markdownDescription": "Specifies the format of the current line blame annotation. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs. Date formatting is controlled by the `#gitlens.currentLine.dateFormat#` setting",
"scope": "window"
},
@ -749,7 +749,7 @@
},
"gitlens.hovers.detailsMarkdownFormat": {
"type": "string",
"default": "${avatar} &nbsp;__${author}__, ${ago} &nbsp; _(${date})_ \n\n${message}\n\n${commands}",
"default": "${avatar} &nbsp;__${author}__${\" via \"pullRequest}, ${ago} &nbsp; _(${date})_ \n\n${message}\n\n${commands}",
"markdownDescription": "Specifies the format (in markdown) of the _commit details_ hover. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs",
"scope": "window"
},
@ -1198,6 +1198,12 @@
"markdownDescription": "Specifies how much (if any) output will be sent to the GitLens output channel",
"scope": "window"
},
"gitlens.pullRequests.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to provide information about the Pull Request (if any) that introduced a commit. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.recentChanges.highlight.locations": {
"type": "array",
"default": [
@ -2386,6 +2392,24 @@
"category": "GitLens"
},
{
"command": "gitlens.connectRemoteProvider",
"title": "Connect to Remote",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-plug.svg",
"light": "images/light/icon-plug.svg"
}
},
{
"command": "gitlens.disconnectRemoteProvider",
"title": "Disconnect from Remote",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-unplug.svg",
"light": "images/light/icon-unplug.svg"
}
},
{
"command": "gitlens.copyMessageToClipboard",
"title": "Copy Commit Message",
"category": "GitLens",
@ -3657,6 +3681,14 @@
"when": "gitlens:enabled"
},
{
"command": "gitlens.connectRemoteProvider",
"when": "false"
},
{
"command": "gitlens.disconnectRemoteProvider",
"when": "false"
},
{
"command": "gitlens.copyMessageToClipboard",
"when": "gitlens:activeFileStatus =~ /blameable/"
},
@ -5196,9 +5228,19 @@
"group": "inline@97"
},
{
"command": "gitlens.connectRemoteProvider",
"when": "viewItem =~ /gitlens:remote\\b(?=.*?\\b\\+disconnected\\b)/",
"group": "inline@98"
},
{
"command": "gitlens.disconnectRemoteProvider",
"when": "viewItem =~ /gitlens:remote\\b(?=.*?\\b\\+connected\\b)/",
"group": "inline@98"
},
{
"command": "gitlens.openRepoInRemote",
"when": "viewItem =~ /gitlens:remote\\b/",
"group": "inline@98"
"group": "inline@99"
},
{
"command": "gitlens.views.fetch",
@ -5236,6 +5278,16 @@
"group": "8_gitlens_actions@1"
},
{
"command": "gitlens.connectRemoteProvider",
"when": "viewItem =~ /gitlens:remote\\b(?=.*?\\b\\+disconnected\\b)/",
"group": "8_gitlens_actions@2"
},
{
"command": "gitlens.disconnectRemoteProvider",
"when": "viewItem =~ /gitlens:remote\\b(?=.*?\\b\\+connected\\b)/",
"group": "8_gitlens_actions@2"
},
{
"command": "gitlens.views.exploreRepoAtRevision",
"when": "viewItem =~ /gitlens:(branch|commit|file\\b((?=.*?\\b\\+committed\\b)|:results)|stash|tag)\\b/",
"group": "3_gitlens_explore@10"
@ -6008,12 +6060,14 @@
"vscode:prepublish": "yarn run bundle"
},
"dependencies": {
"@octokit/graphql": "4.3.1",
"dayjs": "1.8.17",
"iconv-lite": "0.5.0",
"lodash-es": "4.17.15",
"vsls": "0.3.1291"
},
"devDependencies": {
"@types/keytar": "4.4.0",
"@types/lodash-es": "4.17.3",
"@types/node": "10.14.22",
"@types/vscode": "1.37.0",

+ 3
- 5
src/annotations/annotations.ts View File

@ -159,9 +159,8 @@ export class Annotations {
// uri: GitUri,
// editorLine: number,
format: string,
dateFormat: string | null,
scrollable: boolean = true,
getBranchAndTagTips?: (sha: string) => string | undefined
formatOptions?: CommitFormatOptions,
scrollable: boolean = true
): Partial<DecorationOptions> {
// TODO: Enable this once there is better caching
// let diffUris;
@ -170,8 +169,7 @@ export class Annotations {
// }
const message = CommitFormatter.fromTemplate(format, commit, {
dateFormat: dateFormat,
getBranchAndTagTips: getBranchAndTagTips,
...formatOptions,
// previousLineDiffUris: diffUris,
truncateMessageAtNewLine: true
});

+ 52
- 5
src/annotations/lineAnnotationController.ts View File

@ -16,7 +16,7 @@ import { LinesChangeEvent } from '../trackers/gitLineTracker';
import { Annotations } from './annotations';
import { debug, log } from '../system';
import { Logger } from '../logger';
import { CommitFormatter } from '../git/gitService';
import { CommitFormatter, CommitPullRequest, GitRemote } from '../git/gitService';
const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
after: {
@ -140,6 +140,14 @@ export class LineAnnotationController implements Disposable {
editor.setDecorations(annotationDecoration, []);
}
private async getPullRequestForCommit(ref: string, remotes: GitRemote[]) {
try {
return await Container.git.getPullRequestForCommit(ref, remotes, { timeout: 100 });
} catch {
return undefined;
}
}
@debug({ args: false })
private async refresh(editor: TextEditor | undefined) {
if (editor === undefined && this._editor === undefined) return;
@ -206,19 +214,58 @@ export class LineAnnotationController implements Disposable {
getBranchAndTagTips = await Container.git.getBranchesAndTagsTipsFn(trackedDocument.uri.repoPath);
}
let prs;
if (
Container.config.pullRequests.enabled &&
CommitFormatter.has(
cfg.format,
'pullRequest',
'pullRequestAgo',
'pullRequestAgoOrDate',
'pullRequestDate',
'pullRequestState'
)
) {
const promises = [];
let remotes;
for (const l of lines) {
const state = Container.lineTracker.getState(l);
if (state?.commit == null || state.commit.isUncommitted || (remotes != null && remotes.length === 0)) {
continue;
}
if (remotes == null) {
remotes = await Container.git.getRemotes(state.commit.repoPath);
}
promises.push(this.getPullRequestForCommit(state.commit.ref, remotes));
}
prs = new Map<string, CommitPullRequest | undefined>();
for await (const pr of promises) {
if (pr === undefined) continue;
prs.set(pr?.ref, pr);
}
}
const decorations = [];
for (const l of lines) {
const state = Container.lineTracker.getState(l);
if (state === undefined || state.commit === undefined) continue;
if (state?.commit == null) continue;
const decoration = Annotations.trailing(
state.commit,
// await GitUri.fromUri(editor.document.uri),
// l,
cfg.format,
cfg.dateFormat === null ? Container.config.defaultDateFormat : cfg.dateFormat,
cfg.scrollable,
getBranchAndTagTips
{
dateFormat: cfg.dateFormat === null ? Container.config.defaultDateFormat : cfg.dateFormat,
getBranchAndTagTips: getBranchAndTagTips,
pr: prs?.get(state.commit.ref)
},
cfg.scrollable
) as DecorationOptions;
decoration.range = editor.document.validateRange(
new Range(l, Number.MAX_SAFE_INTEGER, l, Number.MAX_SAFE_INTEGER)

+ 1
- 0
src/commands.ts View File

@ -31,6 +31,7 @@ export * from './commands/openRepoInRemote';
export * from './commands/openRevisionFile';
export * from './commands/openWorkingFile';
export * from './commands/gitCommands';
export * from './commands/remoteProviders';
export * from './commands/repositories';
export * from './commands/resetSuppressedWarnings';
export * from './commands/searchCommits';

+ 3
- 1
src/commands/common.ts View File

@ -26,6 +26,7 @@ export enum Commands {
ClearFileAnnotations = 'gitlens.clearFileAnnotations',
CloseUnchangedFiles = 'gitlens.closeUnchangedFiles',
ComputingFileAnnotations = 'gitlens.computingFileAnnotations',
ConnectRemoteProvider = 'gitlens.connectRemoteProvider',
CopyMessageToClipboard = 'gitlens.copyMessageToClipboard',
CopyRemoteFileUrlToClipboard = 'gitlens.copyRemoteFileUrlToClipboard',
CopyShaToClipboard = 'gitlens.copyShaToClipboard',
@ -37,7 +38,6 @@ export enum Commands {
DiffWorkingWith = 'gitlens.diffWorkingWith',
// DEPRECATED
DiffWorkingWithBranch = 'gitlens.diffWorkingWithBranch',
ExternalDiffAll = 'gitlens.externalDiffAll',
DiffWith = 'gitlens.diffWith',
// DEPRECATED
DiffWithBranch = 'gitlens.diffWithBranch',
@ -51,8 +51,10 @@ export enum Commands {
DiffWithWorking = 'gitlens.diffWithWorking',
DiffWithWorkingInDiffRight = 'gitlens.diffWithWorkingInDiffRight',
DiffLineWithWorking = 'gitlens.diffLineWithWorking',
DisconnectRemoteProvider = 'gitlens.disconnectRemoteProvider',
ExploreRepoAtRevision = 'gitlens.exploreRepoAtRevision',
ExternalDiff = 'gitlens.externalDiff',
ExternalDiffAll = 'gitlens.externalDiffAll',
FetchRepositories = 'gitlens.fetchRepositories',
InviteToLiveShare = 'gitlens.inviteToLiveShare',
OpenChangedFiles = 'gitlens.openChangedFiles',

+ 97
- 0
src/commands/remoteProviders.ts View File

@ -0,0 +1,97 @@
'use strict';
import { GitCommit, GitRemote } from '../git/gitService';
import { command, Command, CommandContext, Commands, isCommandViewContextWithRemote } from './common';
import { Container } from '../container';
export interface ConnectRemoteProviderCommandArgs {
remote: string;
repoPath: string;
}
@command()
export class ConnectRemoteProviderCommand extends Command {
static getMarkdownCommandArgs(args: ConnectRemoteProviderCommandArgs): string;
static getMarkdownCommandArgs(remote: GitRemote): string;
static getMarkdownCommandArgs(argsOrRemote: ConnectRemoteProviderCommandArgs | GitRemote): string {
let args: ConnectRemoteProviderCommandArgs | GitCommit;
if (GitRemote.is(argsOrRemote)) {
args = {
remote: argsOrRemote.id,
repoPath: argsOrRemote.repoPath
};
} else {
args = argsOrRemote;
}
return super.getMarkdownCommandArgsCore<ConnectRemoteProviderCommandArgs>(Commands.ConnectRemoteProvider, args);
}
constructor() {
super(Commands.ConnectRemoteProvider);
}
protected preExecute(context: CommandContext, args?: ConnectRemoteProviderCommandArgs) {
if (isCommandViewContextWithRemote(context)) {
args = { ...args, remote: context.node.remote.name, repoPath: context.node.remote.repoPath };
}
return this.execute(args);
}
async execute(args?: ConnectRemoteProviderCommandArgs): Promise<any> {
if (args?.repoPath == null || args?.remote == null) return undefined;
const remote = (await Container.git.getRemotes(args.repoPath)).find(r => args.remote);
if (!remote?.provider?.hasApi()) return undefined;
return remote.provider.connect();
}
}
export interface DisconnectRemoteProviderCommandArgs {
remote: string;
repoPath: string;
}
@command()
export class DisconnectRemoteProviderCommand extends Command {
static getMarkdownCommandArgs(args: DisconnectRemoteProviderCommandArgs): string;
static getMarkdownCommandArgs(remote: GitRemote): string;
static getMarkdownCommandArgs(argsOrRemote: DisconnectRemoteProviderCommandArgs | GitRemote): string {
let args: DisconnectRemoteProviderCommandArgs | GitCommit;
if (GitRemote.is(argsOrRemote)) {
args = {
remote: argsOrRemote.id,
repoPath: argsOrRemote.repoPath
};
} else {
args = argsOrRemote;
}
return super.getMarkdownCommandArgsCore<DisconnectRemoteProviderCommandArgs>(
Commands.DisconnectRemoteProvider,
args
);
}
constructor() {
super(Commands.DisconnectRemoteProvider);
}
protected preExecute(context: CommandContext, args?: ConnectRemoteProviderCommandArgs) {
if (isCommandViewContextWithRemote(context)) {
args = { ...args, remote: context.node.remote.name, repoPath: context.node.remote.repoPath };
}
return this.execute(args);
}
async execute(args?: DisconnectRemoteProviderCommandArgs): Promise<any> {
if (args?.repoPath == null || args?.remote == null) return undefined;
const remote = (await Container.git.getRemotes(args.repoPath)).find(r => args.remote);
if (!remote?.provider?.hasApi()) return undefined;
return remote.provider.disconnect();
}
}

+ 3
- 0
src/config.ts View File

@ -82,6 +82,9 @@ export interface Config {
};
modes: { [key: string]: ModeConfig };
outputLevel: TraceLevel;
pullRequests: {
enabled: boolean;
};
recentChanges: {
highlight: {
locations: HighlightLocations[];

+ 14
- 1
src/container.ts View File

@ -3,12 +3,12 @@ import { commands, ConfigurationChangeEvent, Disposable, ExtensionContext, Uri }
import { Autolinks } from './annotations/autolinks';
import { FileAnnotationController } from './annotations/fileAnnotationController';
import { LineAnnotationController } from './annotations/lineAnnotationController';
import { clearAvatarCache } from './avatars';
import { GitCodeLensController } from './codelens/codeLensController';
import { Commands, ToggleFileBlameCommandArgs } from './commands';
import { AnnotationsToggleMode, Config, configuration, ConfigurationWillChangeEvent } from './configuration';
import { GitFileSystemProvider } from './git/fsProvider';
import { GitService } from './git/gitService';
import { clearAvatarCache } from './avatars';
import { LineHoverController } from './hovers/lineHoverController';
import { Keyboard } from './keyboard';
import { Logger } from './logger';
@ -197,6 +197,19 @@ export class Container {
return this._git;
}
private static _github: Promise<import('./github/github').GitHubApi> | undefined;
static get github() {
if (this._github === undefined) {
this._github = this._loadGitHubApi();
}
return this._github;
}
private static async _loadGitHubApi() {
return new (await import(/* webpackChunkName: "github" */ './github/github')).GitHubApi();
}
private static _keyboard: Keyboard;
static get keyboard() {
return this._keyboard;

+ 92
- 0
src/credentials.ts View File

@ -0,0 +1,92 @@
'use strict';
// eslint-disable-next-line import/no-unresolved
import * as keytarType from 'keytar';
import { Event, EventEmitter } from 'vscode';
import { extensionId } from './constants';
import { Logger } from './logger';
const CredentialKey = `${extensionId}:vscode`;
// keytar depends on a native module shipped in vscode
function getNodeModule<T>(moduleName: string): T | undefined {
// eslint-disable-next-line no-eval
const vscodeRequire = eval('require');
try {
return vscodeRequire(moduleName);
} catch {
return undefined;
}
}
const keychain = getNodeModule<typeof keytarType>('keytar');
interface CredentialSaveEvent {
key: string;
reason: 'save';
}
interface CredentialClearEvent {
key: string | undefined;
reason: 'clear';
}
export type CredentialChangeEvent = CredentialSaveEvent | CredentialClearEvent;
export namespace CredentialManager {
const _onDidChange = new EventEmitter<CredentialChangeEvent>();
export const onDidChange: Event<CredentialChangeEvent> = _onDidChange.event;
export async function addOrUpdate(key: string, value: string | {}) {
if (!key || !value) return;
if (keychain == null) {
Logger.log('CredentialManager.addOrUpdate: No credential store found');
return;
}
try {
await keychain.setPassword(CredentialKey, key, typeof value === 'string' ? value : JSON.stringify(value));
_onDidChange.fire({ key: key, reason: 'save' });
} catch (ex) {
Logger.error(ex, 'CredentialManager.addOrUpdate: Failed to set credentials');
}
}
export async function clear(key: string) {
if (!key) return;
if (keychain == null) {
Logger.log('CredentialManager.clear: No credential store found');
return;
}
try {
await keychain.deletePassword(CredentialKey, key);
_onDidChange.fire({ key: key, reason: 'clear' });
} catch (ex) {
Logger.error(ex, 'CredentialManager.clear: Failed to clear credentials');
}
}
export async function get(key: string): Promise<string | undefined> {
if (!key) return undefined;
if (keychain == null) {
Logger.log('CredentialManager.clear: No credential store found');
return undefined;
}
try {
const value = await keychain.getPassword(CredentialKey, key);
return value ?? undefined;
} catch (ex) {
Logger.error(ex, 'CredentialManager.get: Failed to get credentials');
return undefined;
}
}
export async function getAs<T extends {}>(key: string): Promise<T | undefined> {
const value = await get(key);
if (value == null) return undefined;
return JSON.parse(value) as T;
}
}

+ 133
- 49
src/git/formatters/commitFormatter.ts View File

@ -1,5 +1,6 @@
'use strict';
import {
ConnectRemoteProviderCommand,
DiffWithCommand,
InviteToLiveShareCommand,
OpenCommitInRemoteCommand,
@ -10,12 +11,13 @@ import {
import { DateStyle, FileAnnotationType } from '../../configuration';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { GitCommit, GitLogCommit, GitRemote, GitService, GitUri } from '../gitService';
import { CommitPullRequest, GitCommit, GitLogCommit, GitRemote, GitService, GitUri } from '../gitService';
import { Strings } from '../../system';
import { FormatOptions, Formatter } from './formatter';
import { ContactPresence } from '../../vsls/vsls';
import { getPresenceDataUri } from '../../avatars';
import { emojify } from '../../emojis';
import { Promises } from '../../system/promise';
const emptyStr = '';
@ -27,6 +29,7 @@ export interface CommitFormatOptions extends FormatOptions {
getBranchAndTagTips?: (sha: string) => string | undefined;
line?: number;
markdown?: boolean;
pr?: CommitPullRequest | Promises.CancellationError<CommitPullRequest>;
presence?: ContactPresence;
previousLineDiffUris?: { current: GitUri; previous: GitUri | undefined };
remotes?: GitRemote[];
@ -48,6 +51,11 @@ export interface CommitFormatOptions extends FormatOptions {
email?: Strings.TokenOptions;
id?: Strings.TokenOptions;
message?: Strings.TokenOptions;
pullRequest?: Strings.TokenOptions;
pullRequestAgo?: Strings.TokenOptions;
pullRequestAgoOrDate?: Strings.TokenOptions;
pullRequestDate?: Strings.TokenOptions;
pullRequestState?: Strings.TokenOptions;
tips?: Strings.TokenOptions;
};
}
@ -95,6 +103,26 @@ export class CommitFormatter extends Formatter {
return dateStyle === DateStyle.Absolute ? this._date : this._dateAgo;
}
private get _pullRequestDate() {
const { pr } = this._options;
if (pr == null || pr instanceof Promises.CancellationError) return emptyStr;
return pr.pr?.formatDate(this._options.dateFormat) ?? emptyStr;
}
private get _pullRequestDateAgo() {
const { pr } = this._options;
if (pr == null || pr instanceof Promises.CancellationError) return emptyStr;
return pr.pr?.formatDateFromNow() ?? emptyStr;
}
private get _pullRequestDateOrAgo() {
const dateStyle =
this._options.dateStyle !== undefined ? this._options.dateStyle : Container.config.defaultDateStyle;
return dateStyle === DateStyle.Absolute ? this._pullRequestDate : this._pullRequestDateAgo;
}
get ago() {
return this._padOrTruncate(this._dateAgo, this._options.tokenOptions.ago);
}
@ -129,21 +157,26 @@ export class CommitFormatter extends Formatter {
return emptyStr;
}
let avatar = `![](${this._item
.getGravatarUri(Container.config.defaultGravatarsStyle)
.toString(true)}|width=16,height=16)`;
const presence = this._options.presence;
if (presence != null) {
const title = `${this._item.author} ${this._item.author === 'You' ? 'are' : 'is'} ${
presence.status === 'dnd' ? 'in ' : ''
presence.status === 'dnd' ? 'in ' : emptyStr
}${presence.statusText.toLocaleLowerCase()}`;
avatar += `![${title}](${getPresenceDataUri(presence.status)})`;
avatar = `[${avatar}](# "${title}")`;
return `${this._getGravatarMarkdown(title)}${this._getPresenceMarkdown(presence, title)}`;
}
return avatar;
return this._getGravatarMarkdown(this._item.author);
}
private _getGravatarMarkdown(title: string) {
return `![${title}](${this._item
.getGravatarUri(Container.config.defaultGravatarsStyle)
.toString(true)}|width=16,height=16 "${title}")`;
}
private _getPresenceMarkdown(presence: ContactPresence, title: string) {
return `![${title}](${getPresenceDataUri(presence.status)} "${title}")`;
}
get changes() {
@ -178,13 +211,13 @@ export class CommitFormatter extends Formatter {
this._options.tokenOptions.id
)}\``;
commands += ` **[\`${GlyphChars.MuchLessThan}\`](${DiffWithCommand.getMarkdownCommandArgs({
commands += `&nbsp; **[\`${GlyphChars.MuchLessThan}\`](${DiffWithCommand.getMarkdownCommandArgs({
lhs: {
sha: diffUris.previous.sha || '',
sha: diffUris.previous.sha || emptyStr,
uri: diffUris.previous.documentUri()
},
rhs: {
sha: diffUris.current.sha || '',
sha: diffUris.current.sha || emptyStr,
uri: diffUris.current.documentUri()
},
repoPath: this._item.repoPath,
@ -202,14 +235,35 @@ export class CommitFormatter extends Formatter {
return commands;
}
const separator = ' &nbsp;';
commands = `[\`${this.id}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(
this._item.sha
)} "Show Commit Details") `;
)} "Show Commit Details")${separator}`;
const { pr } = this._options;
if (pr != null) {
if (pr instanceof Promises.CancellationError) {
commands += `[\`PR (loading${GlyphChars.Ellipsis})\`](# "Searching for a Pull Request (if any) that introduced this commit...")${separator}`;
} else if (pr.pr != null) {
commands += `[\`PR #${pr.pr.number}\`](${pr.pr.url} "Open Pull Request \\#${pr.pr.number}${
pr.remote?.provider != null ? ` on ${pr.remote.provider.name}` : ''
}\n${GlyphChars.Dash.repeat(2)}\n${pr.pr.title}\n${
pr.pr.state
}, ${pr.pr.formatDateFromNow()}")${separator}`;
} else if (pr.remote?.provider != null) {
commands += `[\`Connect to ${pr.remote.provider.name}${
GlyphChars.Ellipsis
}\`](${ConnectRemoteProviderCommand.getMarkdownCommandArgs(pr.remote)} "Connect to ${
pr.remote.provider.name
} to enable the display of the Pull Request (if any) that introduced this commit")${separator}`;
}
}
commands += `**[\`${GlyphChars.MuchLessThan}\`](${DiffWithCommand.getMarkdownCommandArgs(
this._item,
this._options.line
)} "Open Changes")** `;
)} "Open Changes")** class="nx">${separator}`;
if (this._item.previousSha !== undefined) {
let annotationType = this._options.annotationType;
@ -226,13 +280,13 @@ export class CommitFormatter extends Formatter {
uri,
annotationType || FileAnnotationType.Blame,
this._options.line
)} "Blame Previous Revision")** `;
)} "Blame Previous Revision")** class="nx">${separator}`;
}
if (this._options.remotes !== undefined && this._options.remotes.length !== 0) {
commands += `**[\` ${GlyphChars.ArrowUpRight} \`](${OpenCommitInRemoteCommand.getMarkdownCommandArgs(
this._item.sha
)} "Open on Remote")** `;
)} "Open on Remote")** class="nx">${separator}`;
}
if (this._item.author !== 'You') {
@ -240,7 +294,7 @@ export class CommitFormatter extends Formatter {
if (presence != null) {
commands += `[\` ${GlyphChars.Envelope}+ \`](${InviteToLiveShareCommand.getMarkdownCommandArgs(
this._item.email
)} "Invite ${this._item.author} (${presence.statusText}) to a Live Share Session") `;
)} "Invite ${this._item.author} (${presence.statusText}) to a Live Share Session") class="nx">${separator}`;
}
}
@ -276,31 +330,25 @@ export class CommitFormatter extends Formatter {
}
get message() {
let message: string;
if (this._item.isUncommitted) {
if (
this._item.isUncommittedStaged ||
(this._options.previousLineDiffUris !== undefined &&
this._options.previousLineDiffUris.current.isUncommittedStaged)
) {
message = 'Staged changes';
} else {
message = 'Uncommitted changes';
}
} else {
if (this._options.truncateMessageAtNewLine) {
const index = this._item.message.indexOf('\n');
message =
index === -1
? this._item.message
: `${this._item.message.substring(0, index)}${GlyphChars.Space}${GlyphChars.Ellipsis}`;
} else {
message = this._item.message;
}
const staged =
this._item.isUncommittedStaged || this._options.previousLineDiffUris?.current?.isUncommittedStaged;
return this._padOrTruncate(
`${this._options.markdown ? '\n> ' : ''}${staged ? 'Staged' : 'Uncommitted'} changes`,
this._options.tokenOptions.message
);
}
message = emojify(message);
let message = this._item.message;
if (this._options.truncateMessageAtNewLine) {
const index = message.indexOf('\n');
if (index !== -1) {
message = `${message.substring(0, index)}${GlyphChars.Space}${GlyphChars.Ellipsis}`;
}
}
message = emojify(message);
message = this._padOrTruncate(message, this._options.tokenOptions.message);
if (!this._options.markdown) {
@ -312,6 +360,47 @@ export class CommitFormatter extends Formatter {
return `\n> ${message}`;
}
get pullRequest() {
const { pr } = this._options;
if (pr == null) return emptyStr;
let text;
if (pr instanceof Promises.CancellationError) {
text = this._options.markdown
? `[PR (loading${GlyphChars.Ellipsis})](# "Searching for a Pull Request (if any) that introduced this commit...")`
: `PR (loading${GlyphChars.Ellipsis})`;
} else if (pr.pr != null) {
text = this._options.markdown
? `[PR #${pr.pr.number}](${pr.pr.url} "Open Pull Request \\#${pr.pr.number}${
pr.remote?.provider != null ? ` on ${pr.remote.provider.name}` : ''
}\n${GlyphChars.Dash.repeat(2)}\n${pr.pr.title}\n${pr.pr.state}, ${pr.pr.formatDateFromNow()}")`
: `PR #${pr.pr.number}`;
} else {
return emptyStr;
}
return this._padOrTruncate(text, this._options.tokenOptions.pullRequest);
}
get pullRequestAgo() {
return this._padOrTruncate(this._pullRequestDateAgo, this._options.tokenOptions.pullRequestAgo);
}
get pullRequestAgoOrDate() {
return this._padOrTruncate(this._pullRequestDateOrAgo, this._options.tokenOptions.pullRequestAgoOrDate);
}
get pullRequestDate() {
return this._padOrTruncate(this._pullRequestDate, this._options.tokenOptions.pullRequestDate);
}
get pullRequestState() {
const { pr } = this._options;
if (pr == null || pr instanceof Promises.CancellationError) return emptyStr;
return this._padOrTruncate(pr.pr?.state ?? emptyStr, this._options.tokenOptions.pullRequestState);
}
get sha() {
return this.id;
}
@ -338,7 +427,12 @@ export class CommitFormatter extends Formatter {
return super.fromTemplateCore(this, template, commit, dateFormatOrOptions);
}
static has(format: string, token: string) {
static has(format: string, ...tokens: (keyof NonNullable<CommitFormatOptions['tokenOptions']>)[]) {
const token =
tokens.length === 1
? tokens[0]
: (`(${tokens.join('|')})` as keyof NonNullable<CommitFormatOptions['tokenOptions']>);
let regex = hasTokenRegexMap.get(token);
if (regex === undefined) {
regex = new RegExp(`\\b${token}\\b`);
@ -348,13 +442,3 @@ export class CommitFormatter extends Formatter {
return regex.test(format);
}
}
// const autolinks = new Autolinks();
// const text = autolinks.linkify(`\\#756
// foo
// bar
// baz \\#756
// boo\\#789
// \\#666
// gh\\-89 gh\\-89gh\\-89 GH\\-89`);
// console.log(text);

+ 90
- 1
src/git/gitService.ts View File

@ -25,7 +25,18 @@ import { CommandContext, DocumentSchemes, setCommandContext } from '../constants
import { Container } from '../container';
import { LogCorrelationContext, Logger } from '../logger';
import { Messages } from '../messages';
import { Arrays, debug, gate, Iterables, log, Objects, Strings, TernarySearchTree, Versions } from '../system';
import {
Arrays,
debug,
gate,
Iterables,
log,
Objects,
Promises,
Strings,
TernarySearchTree,
Versions
} from '../system';
import { CachedBlame, CachedDiff, CachedLog, GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker';
import { vslsUriPrefixRegex } from '../vsls/vsls';
import {
@ -73,6 +84,7 @@ import { GitUri } from './gitUri';
import { RemoteProviderFactory, RemoteProviders } from './remotes/factory';
import { GitReflogParser, GitShortLogParser } from './parsers/parsers';
import { isWindows } from './shell';
import { PullRequest, PullRequestDateFormatting } from './models/models';
export * from './gitUri';
export * from './models/models';
@ -80,6 +92,7 @@ export * from './formatters/formatters';
export * from './remotes/provider';
export { RemoteProviderFactory } from './remotes/factory';
const emptyArray = (Object.freeze([]) as any) as any[];
const emptyStr = '';
const slash = '/';
@ -97,6 +110,12 @@ const searchOperationRegex = /((?:=|message|@|author|#|commit|\?|file|~|change):
const emptyPromise: Promise<GitBlame | GitDiff | GitLog | undefined> = Promise.resolve(undefined);
const reflogCommands = ['merge', 'pull'];
export interface CommitPullRequest {
ref: string;
pr: PullRequest | undefined;
remote: GitRemote | undefined;
}
export type SearchOperators =
| ''
| '=:'
@ -253,6 +272,7 @@ export class GitService implements Disposable {
) {
BranchDateFormatting.reset();
CommitDateFormatting.reset();
PullRequestDateFormatting.reset();
}
}
@ -2331,6 +2351,75 @@ export class GitService implements Disposable {
return GitUri.fromFile(file || fileName, repoPath, previousRef || GitService.deletedOrMissingSha);
}
async getPullRequestForCommit(
ref: string,
remotes: GitRemote[],
{ timeout }: { timeout?: number } = {}
): Promise<CommitPullRequest | undefined> {
if (
!Container.config.pullRequests.enabled ||
(remotes != null && remotes.length === 0) ||
Git.isUncommitted(ref)
) {
return undefined;
}
const pr: CommitPullRequest = {
ref: ref,
pr: undefined,
remote: undefined
};
const prs: Promise<[PullRequest | undefined, GitRemote]>[] = [];
let foundConnectedDefaultSkipOthers = false;
for (const remote of GitRemote.sort(remotes)) {
if (!remote.provider?.hasApi()) continue;
if (!(await remote.provider.isConnected())) {
if (pr.remote == null) {
pr.remote = remote;
}
continue;
}
if (!foundConnectedDefaultSkipOthers) {
if (remote.default) {
foundConnectedDefaultSkipOthers = true;
}
const requestOrPR = remote.provider.getPullRequestForCommit(ref);
if (requestOrPR == null || !Promises.is(requestOrPR)) {
pr.pr = requestOrPR;
pr.remote = requestOrPR !== undefined ? remote : undefined;
break;
}
prs.push(requestOrPR.then(pr => [pr, remote]));
} else if (pr.remote !== undefined) {
break;
}
}
if (prs.length !== 0) {
pr.remote = undefined;
let promise = Promises.first(prs, ([pr]) => pr != null);
if (timeout != null && timeout > 0) {
promise = Promises.cancellable(promise, timeout);
}
try {
[pr.pr, pr.remote] = (await promise) ?? emptyArray;
} catch (ex) {
if (ex instanceof Promises.CancellationError) {
throw ex;
}
}
}
return pr;
}
@log()
async getIncomingActivity(
repoPath: string,

+ 1
- 0
src/git/models/models.ts View File

@ -39,6 +39,7 @@ export * from './diff';
export * from './file';
export * from './log';
export * from './logCommit';
export * from './pullRequest';
export * from './remote';
export * from './repository';
export * from './reflog';

+ 109
- 0
src/git/models/pullRequest.ts View File

@ -0,0 +1,109 @@
'use strict';
import { configuration, DateStyle } from '../../configuration';
import { Dates, memoize } from '../../system';
export const PullRequestDateFormatting = {
dateFormat: undefined! as string | null,
dateStyle: undefined! as DateStyle,
reset: () => {
PullRequestDateFormatting.dateFormat = configuration.get('defaultDateFormat');
PullRequestDateFormatting.dateStyle = configuration.get('defaultDateStyle');
}
};
export enum PullRequestState {
Open = 'Open',
Closed = 'Closed',
Merged = 'Merged'
}
export class PullRequest {
constructor(
public readonly number: number,
public readonly title: string,
public readonly url: string,
public readonly state: PullRequestState,
public readonly date: Date,
public readonly closedDate?: Date,
public readonly mergedDate?: Date
) {}
get formattedDate(): string {
return PullRequestDateFormatting.dateStyle === DateStyle.Absolute
? this.formatDate(PullRequestDateFormatting.dateFormat)
: this.formatDateFromNow();
}
@memoize()
private get dateFormatter(): Dates.DateFormatter {
return Dates.getFormatter(this.mergedDate ?? this.closedDate ?? this.date);
}
@memoize<PullRequest['formatDate']>(format => (format == null ? 'MMMM Do, YYYY h:mma' : format))
formatDate(format?: string | null) {
if (format == null) {
format = 'MMMM Do, YYYY h:mma';
}
return this.dateFormatter.format(format);
}
formatDateFromNow() {
return this.dateFormatter.fromNow();
}
@memoize()
private get closedDateFormatter(): Dates.DateFormatter | undefined {
return this.closedDate === undefined ? undefined : Dates.getFormatter(this.closedDate);
}
@memoize<PullRequest['formatClosedDate']>(format => (format == null ? 'MMMM Do, YYYY h:mma' : format))
formatClosedDate(format?: string | null) {
if (format == null) {
format = 'MMMM Do, YYYY h:mma';
}
return this.closedDateFormatter?.format(format) ?? '';
}
formatClosedDateFromNow() {
return this.closedDateFormatter?.fromNow() ?? '';
}
@memoize()
private get mergedDateFormatter(): Dates.DateFormatter | undefined {
return this.mergedDate === undefined ? undefined : Dates.getFormatter(this.mergedDate);
}
@memoize<PullRequest['formatMergedDate']>(format => (format == null ? 'MMMM Do, YYYY h:mma' : format))
formatMergedDate(format?: string | null) {
if (format == null) {
format = 'MMMM Do, YYYY h:mma';
}
return this.mergedDateFormatter?.format(format) ?? '';
}
formatMergedDateFromNow() {
return this.mergedDateFormatter?.fromNow() ?? '';
}
@memoize()
private get updatedDateFormatter(): Dates.DateFormatter {
return Dates.getFormatter(this.date);
}
@memoize<PullRequest['formatUpdatedDate']>(format => (format == null ? 'MMMM Do, YYYY h:mma' : format))
formatUpdatedDate(format?: string | null) {
if (format == null) {
format = 'MMMM Do, YYYY h:mma';
}
return this.updatedDateFormatter.format(format);
}
formatUpdatedDateFromNow() {
return this.updatedDateFormatter.fromNow();
}
}

+ 2
- 2
src/git/models/remote.ts View File

@ -1,7 +1,7 @@
'use strict';
import { WorkspaceState } from '../../constants';
import { Container } from '../../container';
import { RemoteProvider } from '../remotes/factory';
import { RemoteProvider, RemoteProviderWithApi } from '../remotes/factory';
export enum GitRemoteType {
Fetch = 'fetch',
@ -28,7 +28,7 @@ export class GitRemote {
public readonly scheme: string,
public readonly domain: string,
public readonly path: string,
public readonly provider: RemoteProvider | undefined,
public readonly provider: RemoteProvider | RemoteProviderWithApi | undefined,
public readonly types: { type: GitRemoteType; url: string }[]
) {}

+ 32
- 7
src/git/models/repository.ts View File

@ -16,13 +16,12 @@ import {
import { configuration } from '../../configuration';
import { StarredRepositories, WorkspaceState } from '../../constants';
import { Container } from '../../container';
import { Functions, gate, log } from '../../system';
import { Functions, gate, Iterables, log, logName } from '../../system';
import { GitBranch, GitContributor, GitDiffShortStat, GitRemote, GitStash, GitStatus, GitTag } from '../git';
import { GitUri } from '../gitUri';
import { RemoteProviderFactory, RemoteProviders } from '../remotes/factory';
import { RemoteProviderFactory, RemoteProviders, RemoteProviderWithApi } from '../remotes/factory';
import { Messages } from '../../messages';
import { Logger } from '../../logger';
import { logName } from '../../system/decorators/log';
import { runGitCommandInTerminal } from '../../terminal';
export enum RepositoryChange {
@ -91,6 +90,7 @@ export class Repository implements Disposable {
private _pendingChanges: { repo?: RepositoryChangeEvent; fs?: RepositoryFileSystemChangeEvent } = {};
private _providers: RemoteProviders | undefined;
private _remotes: Promise<GitRemote[]> | undefined;
private _remotesDisposable: Disposable | undefined;
private _suspended: boolean;
constructor(
@ -161,7 +161,8 @@ export class Repository implements Disposable {
// }
// }
this._disposable && this._disposable.dispose();
this._remotesDisposable?.dispose();
this._disposable?.dispose();
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
@ -169,7 +170,7 @@ export class Repository implements Disposable {
this._providers = RemoteProviderFactory.loadProviders(configuration.get('remotes', this.folder.uri));
if (!configuration.initializing(e)) {
this._remotes = undefined;
this.resetRemotesCache();
this.fireChange(RepositoryChange.Remotes);
}
}
@ -192,7 +193,7 @@ export class Repository implements Disposable {
this._branch = undefined;
if (uri !== undefined && uri.path.endsWith('refs/remotes')) {
this._remotes = undefined;
this.resetRemotesCache();
this.fireChange(RepositoryChange.Remotes);
return;
@ -205,7 +206,7 @@ export class Repository implements Disposable {
}
if (uri !== undefined && uri.path.endsWith('config')) {
this._remotes = undefined;
this.resetRemotesCache();
this.fireChange(RepositoryChange.Config, RepositoryChange.Remotes);
return;
@ -347,11 +348,35 @@ export class Repository implements Disposable {
// Since we are caching the results, always sort
this._remotes = Container.git.getRemotesCore(this.path, this._providers, { sort: true });
this.subscribeToRemotes(this._remotes);
}
return this._remotes;
}
private resetRemotesCache() {
this._remotes = undefined;
if (this._remotesDisposable !== undefined) {
this._remotesDisposable.dispose();
this._remotesDisposable = undefined;
}
}
private async subscribeToRemotes(remotes: Promise<GitRemote[]>) {
if (this._remotesDisposable !== undefined) {
this._remotesDisposable.dispose();
this._remotesDisposable = undefined;
}
this._remotesDisposable = Disposable.from(
...Iterables.filterMap(await remotes, r => {
if (!(r.provider instanceof RemoteProviderWithApi)) return undefined;
return r.provider.onDidChange(() => this.fireChange(RepositoryChange.Remotes));
})
);
}
getStashList(): Promise<GitStash | undefined> {
return Container.git.getStashList(this.path);
}

+ 1
- 1
src/git/remotes/azure-devops.ts View File

@ -43,7 +43,7 @@ export class AzureDevOpsRemote extends RemoteProvider {
{
prefix: '#',
url: `${baseUrl}/_workitems/edit/<num>`,
title: 'Open Work Item #<num>'
title: `Open Work Item #<num> on ${this.name}`
}
];
}

+ 2
- 2
src/git/remotes/bitbucket-server.ts View File

@ -16,12 +16,12 @@ export class BitbucketServerRemote extends RemoteProvider {
{
prefix: 'issue #',
url: `${this.baseUrl}/issues/<num>`,
title: 'Open Issue #<num>'
title: `Open Issue #<num> on ${this.name}`
},
{
prefix: 'pull request #',
url: `${this.baseUrl}/pull-requests/<num>`,
title: 'Open PR #<num>'
title: `Open PR #<num> on ${this.name}`
}
];
}

+ 2
- 2
src/git/remotes/bitbucket.ts View File

@ -16,12 +16,12 @@ export class BitbucketRemote extends RemoteProvider {
{
prefix: 'issue #',
url: `${this.baseUrl}/issues/<num>`,
title: 'Open Issue #<num>'
title: `Open Issue #<num> on ${this.name}`
},
{
prefix: 'pull request #',
url: `${this.baseUrl}/pull-requests/<num>`,
title: 'Open PR #<num>'
title: `Open PR #<num> on ${this.name}`
}
];
}

+ 5
- 3
src/git/remotes/factory.ts View File

@ -7,9 +7,9 @@ import { BitbucketServerRemote } from './bitbucket-server';
import { CustomRemote } from './custom';
import { GitHubRemote } from './github';
import { GitLabRemote } from './gitlab';
import { RemoteProvider } from './provider';
import { RemoteProvider, RemoteProviderWithApi } from './provider';
export { RemoteProvider };
export { RemoteProvider, RemoteProviderWithApi };
export type RemoteProviders = [string | RegExp, (domain: string, path: string) => RemoteProvider][];
const defaultProviders: RemoteProviders = [
@ -62,6 +62,7 @@ export class RemoteProviderFactory {
}
providers.push(...defaultProviders);
return providers;
}
@ -80,7 +81,8 @@ export class RemoteProviderFactory {
return (domain: string, path: string) => new GitHubRemote(domain, path, cfg.protocol, cfg.name, true);
case CustomRemoteType.GitLab:
return (domain: string, path: string) => new GitLabRemote(domain, path, cfg.protocol, cfg.name, true);
default:
return undefined;
}
return undefined;
}
}

+ 73
- 19
src/git/remotes/github.ts View File

@ -1,16 +1,32 @@
'use strict';
import { Range } from 'vscode';
import { RemoteProvider } from './provider';
import { AutolinkReference } from '../../config';
import { Disposable, env, QuickInputButton, Range, Uri, window } from 'vscode';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
import { AutolinkReference } from '../../config';
import { Container } from '../../container';
import { PullRequest } from '../models/pullRequest';
import { RemoteProviderWithApi } from './provider';
const issueEnricher3rdParyRegex = /\b(\w+\\?-?\w+(?!\\?-)\/\w+\\?-?\w+(?!\\?-))\\?#([0-9]+)\b/g;
export class GitHubRemote extends RemoteProvider {
export class GitHubRemote extends RemoteProviderWithApi<{ token: string }> {
private readonly Buttons = class {
static readonly Help: QuickInputButton = {
iconPath: {
dark: Uri.file(Container.context.asAbsolutePath('images/dark/icon-help.svg')),
light: Uri.file(Container.context.asAbsolutePath('images/light/icon-help.svg'))
},
tooltip: 'Help'
};
};
constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) {
super(domain, path, protocol, name, custom);
}
get apiBaseUrl() {
return this.custom ? `${this.protocol}://${this.domain}/api` : `https://api.${this.domain}`;
}
private _autolinks: (AutolinkReference | DynamicAutolinkReference)[] | undefined;
get autolinks(): (AutolinkReference | DynamicAutolinkReference)[] {
if (this._autolinks === undefined) {
@ -18,19 +34,19 @@ export class GitHubRemote extends RemoteProvider {
{
prefix: '#',
url: `${this.baseUrl}/issues/<num>`,
title: 'Open Issue #<num>'
title: `Open Issue #<num> on ${this.name}`
},
{
prefix: 'gh-',
url: `${this.baseUrl}/issues/<num>`,
title: 'Open Issue #<num>',
title: `Open Issue #<num> on ${this.name}`,
ignoreCase: true
},
{
linkify: (text: string) =>
text.replace(
issueEnricher3rdParyRegex,
`[$&](${this.protocol}://${this.domain}/$1/issues/$2 "Open Issue #$2 from $1")`
`[$&](${this.protocol}://${this.domain}/$1/issues/$2 "Open Issue #$2 from $1 on ${this.name}")`
)
}
];
@ -46,18 +62,48 @@ export class GitHubRemote extends RemoteProvider {
return this.formatName('GitHub');
}
// enrichMessage(message: string): string {
// return (
// message
// // Matches #123 or gh-123 or GH-123
// .replace(issueEnricherRegex, `$1[$2](${this.baseUrl}/issues/$3 "Open Issue $2")`)
// // Matches eamodio/vscode-gitlens#123
// .replace(
// issueEnricher3rdParyRegex,
// `[$&](${this.protocol}://${this.domain}/$1/issues/$2 "Open Issue #$2 from $1")`
// )
// );
// }
async connect() {
const input = window.createInputBox();
input.ignoreFocusOut = true;
let disposable: Disposable | undefined;
let token: string | undefined;
try {
token = await new Promise<string | undefined>(resolve => {
disposable = Disposable.from(
input.onDidHide(() => resolve(undefined)),
input.onDidTriggerButton(e => {
if (e === this.Buttons.Help) {
env.openExternal(Uri.parse('https://github.com/eamodio/vscode-gitlens/wiki'));
}
}),
input.onDidChangeValue(
e =>
(input.validationMessage =
e == null || e.length === 0
? 'Must be a valid GitHub personal access token'
: undefined)
),
input.onDidAccept(() => resolve(input.value))
);
input.buttons = [this.Buttons.Help];
input.title = `Connect to ${this.name}`;
input.prompt = 'Enter a GitHub personal access token';
input.placeholder = 'Generate a personal access token from github.com (required)';
input.show();
});
} finally {
input.dispose();
disposable?.dispose();
}
if (token == null || token.length === 0) return;
this.saveCredentials({ token: token });
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
@ -87,4 +133,12 @@ export class GitHubRemote extends RemoteProvider {
if (branch) return `${this.baseUrl}/blob/${branch}/${fileName}${line}`;
return `${this.baseUrl}?path=${fileName}${line}`;
}
protected async onGetPullRequestForCommit(
{ token }: { token: string },
ref: string
): Promise<PullRequest | undefined> {
const [owner, repo] = this.splitPath();
return (await Container.github).getPullRequestForCommit(token, owner, repo, ref, { baseUrl: this.apiBaseUrl });
}
}

+ 1
- 1
src/git/remotes/gitlab.ts View File

@ -16,7 +16,7 @@ export class GitLabRemote extends RemoteProvider {
{
prefix: '#',
url: `${this.baseUrl}/issues/<num>`,
title: 'Open Issue #<num>'
title: `Open Issue #<num> on ${this.name}`
}
];
}

+ 155
- 35
src/git/remotes/provider.ts View File

@ -1,10 +1,14 @@
'use strict';
import { env, Range, Uri, window } from 'vscode';
import { env, Event, EventEmitter, Range, Uri, window } from 'vscode';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
import { AutolinkReference } from '../../config';
import { Container } from '../../container';
import { CredentialChangeEvent, CredentialManager } from '../../credentials';
import { Logger } from '../../logger';
import { Messages } from '../../messages';
import { GitLogCommit } from '../models/logCommit';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
import { PullRequest } from '../models/pullRequest';
import { debug, Promises } from '../../system';
export enum RemoteResourceType {
Branch = 'branch',
@ -65,7 +69,7 @@ export function getNameFromRemoteResource(resource: RemoteResource) {
}
export abstract class RemoteProvider {
private _name: string | undefined;
protected _name: string | undefined;
constructor(
public readonly domain: string,
@ -81,43 +85,15 @@ export abstract class RemoteProvider {
return [];
}
get icon(): string {
return 'remote';
}
get displayPath(): string {
return this.path;
}
abstract get name(): string;
protected get baseUrl() {
return `${this.protocol}://${this.domain}/${this.path}`;
}
protected formatName(name: string) {
if (this._name !== undefined) return this._name;
return `${name}${this.custom ? ` (${this.domain})` : ''}`;
}
protected splitPath(): [string, string] {
const index = this.path.indexOf('/');
return [this.path.substring(0, index), this.path.substring(index + 1)];
}
protected getUrlForRepository(): string {
return this.baseUrl;
get icon(): string {
return 'remote';
}
protected abstract getUrlForBranches(): string;
protected abstract getUrlForBranch(branch: string): string;
protected abstract getUrlForCommit(sha: string): string;
protected abstract getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string;
private openUrl(url?: string): Thenable<{} | undefined> {
if (url === undefined) return Promise.resolve(undefined);
return env.openExternal(Uri.parse(url));
}
abstract get name(): string;
async copy(resource: RemoteResource): Promise<{} | undefined> {
const url = this.url(resource);
@ -140,6 +116,10 @@ export abstract class RemoteProvider {
}
}
hasApi(): this is RemoteProviderWithApi {
return RemoteProviderWithApi.is(this);
}
open(resource: RemoteResource): Thenable<{} | undefined> {
return this.openUrl(this.url(resource));
}
@ -168,8 +148,148 @@ export abstract class RemoteProvider {
resource.sha !== undefined ? encodeURIComponent(resource.sha) : undefined,
resource.range
);
default:
return undefined;
}
}
protected get baseUrl() {
return `${this.protocol}://${this.domain}/${this.path}`;
}
return undefined;
protected formatName(name: string) {
if (this._name != null) return this._name;
return `${name}${this.custom ? ` (${this.domain})` : ''}`;
}
protected splitPath(): [string, string] {
const index = this.path.indexOf('/');
return [this.path.substring(0, index), this.path.substring(index + 1)];
}
protected abstract getUrlForBranch(branch: string): string;
protected abstract getUrlForBranches(): string;
protected abstract getUrlForCommit(sha: string): string;
protected abstract getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string;
protected getUrlForRepository(): string {
return this.baseUrl;
}
private openUrl(url?: string): Thenable<{} | undefined> {
if (url === undefined) return Promise.resolve(undefined);
return env.openExternal(Uri.parse(url));
}
}
export abstract class RemoteProviderWithApi<T extends string | {} = any> extends RemoteProvider {
static is(provider: RemoteProvider | undefined): provider is RemoteProviderWithApi {
return provider instanceof RemoteProviderWithApi;
}
private readonly _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> {
return this._onDidChange.event;
}
constructor(domain: string, path: string, protocol?: string, name?: string, custom?: boolean) {
super(domain, path, protocol, name, custom);
Container.context.subscriptions.push(CredentialManager.onDidChange(this.onCredentialsChanged, this));
}
private onCredentialsChanged(e: CredentialChangeEvent) {
if (e.reason === 'save' && e.key === this.credentialsKey) {
if (this._credentials === null) {
this._credentials = undefined;
}
this._onDidChange.fire();
return;
}
if (e.reason === 'clear' && (e.key === undefined || e.key === this.credentialsKey)) {
this._credentials = undefined;
this._prsByCommit.clear();
this._onDidChange.fire();
}
}
abstract get apiBaseUrl(): string;
abstract async connect(): Promise<void>;
disconnect(): Promise<void> {
this._prsByCommit.clear();
return this.clearCredentials();
}
async isConnected(): Promise<boolean> {
return (await this.credentials()) != null;
}
private _prsByCommit = new Map<string, Promise<PullRequest | null> | PullRequest | null>();
@debug()
getPullRequestForCommit(ref: string): Promise<PullRequest | undefined> | PullRequest | undefined {
let pr = this._prsByCommit.get(ref);
if (pr === undefined) {
pr = this.getPullRequestForCommitCore(ref);
this._prsByCommit.set(ref, pr);
}
if (pr == null || !Promises.is(pr)) return pr ?? undefined;
return pr.then(pr => pr ?? undefined);
}
protected abstract onGetPullRequestForCommit(credentials: T, ref: string): Promise<PullRequest | undefined>;
protected _credentials: T | null | undefined;
protected credentials() {
if (this._credentials === undefined) {
return CredentialManager.getAs<T>(this.credentialsKey).then(c => {
this._credentials = c ?? null;
return c ?? undefined;
});
}
return this._credentials ?? undefined;
}
protected async clearCredentials() {
this._credentials = undefined;
await CredentialManager.clear(this.credentialsKey);
this._credentials = undefined;
}
protected saveCredentials(credentials: T) {
this._credentials = credentials;
return CredentialManager.addOrUpdate(this.credentialsKey, credentials);
}
private get credentialsKey() {
return this.custom ? `${this.name}:${this.domain}` : this.name;
}
@debug()
private async getPullRequestForCommitCore(ref: string) {
const cc = Logger.getCorrelationContext();
if (!(await this.isConnected())) return null;
try {
const pr = (await this.onGetPullRequestForCommit(this._credentials!, ref)) ?? null;
this._prsByCommit.set(ref, pr);
return pr;
} catch (ex) {
Logger.error(ex, cc);
this._prsByCommit.delete(ref);
return null;
}
}
}

+ 92
- 0
src/github/github.ts View File

@ -0,0 +1,92 @@
'use strict';
import { graphql } from '@octokit/graphql';
import { Logger } from '../logger';
import { debug } from '../system';
import { PullRequest, PullRequestState } from '../git/gitService';
export class GitHubApi {
@debug()
async getPullRequestForCommit(
token: string,
owner: string,
repo: string,
ref: string,
options?: {
baseUrl?: string;
}
): Promise<PullRequest | undefined> {
const cc = Logger.getCorrelationContext();
try {
const query = `query pr($owner: String!, $repo: String!, $sha: String!) {
repository(name: $repo, owner: $owner) {
object(expression: $sha) {
... on Commit {
associatedPullRequests(first: 1, orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
permalink
number
title
state
updatedAt
closedAt
mergedAt
repository {
owner {
login
}
}
}
}
}
}
}
}`;
const variables = { owner: owner, repo: repo, sha: ref };
Logger.debug(cc, `variables: ${JSON.stringify(variables)}`);
const rsp = await graphql(query, {
...variables,
headers: { authorization: `token ${token}` },
...options
});
const pr = rsp?.repository?.object?.associatedPullRequests?.nodes?.[0] as GitHubPullRequest | undefined;
if (pr == null) return undefined;
// GitHub seems to sometimes return PRs for forks
if (pr.repository.owner.login !== owner) return undefined;
return new PullRequest(
pr.number,
pr.title,
pr.permalink,
pr.state === 'MERGED'
? PullRequestState.Merged
: pr.state === 'CLOSED'
? PullRequestState.Closed
: PullRequestState.Open,
new Date(pr.updatedAt),
pr.closedAt == null ? undefined : new Date(pr.closedAt),
pr.mergedAt == null ? undefined : new Date(pr.mergedAt)
);
} catch (ex) {
Logger.error(ex, cc);
throw ex;
}
}
}
interface GitHubPullRequest {
permalink: string;
number: number;
title: string;
state: 'OPEN' | 'CLOSED' | 'MERGED';
updatedAt: string;
closedAt: string | null;
mergedAt: string | null;
repository: {
owner: {
login: string;
};
};
}

+ 19
- 3
src/hovers/hovers.ts View File

@ -10,9 +10,11 @@ import {
GitCommit,
GitDiffHunkLine,
GitLogCommit,
GitRemote,
GitService,
GitUri
} from '../git/gitService';
import { Promises } from '../system/promise';
export namespace Hovers {
export async function changesMessage(
@ -150,10 +152,12 @@ export namespace Hovers {
dateFormat = 'MMMM Do, YYYY h:mma';
}
const [presence, previousLineDiffUris, remotes] = await Promise.all([
Container.vsls.maybeGetPresence(commit.email).catch(reason => undefined),
const remotes = await Container.git.getRemotes(commit.repoPath, { sort: true });
const [previousLineDiffUris, pr, presence] = await Promise.all([
commit.isUncommitted ? commit.getPreviousLineDiffUris(uri, editorLine, uri.sha) : undefined,
Container.git.getRemotes(commit.repoPath, { sort: true })
getPullRequestForCommit(commit.ref, remotes),
Container.vsls.maybeGetPresence(commit.email).catch(reason => undefined)
]);
const details = CommitFormatter.fromTemplate(Container.config.hovers.detailsMarkdownFormat, commit, {
@ -161,6 +165,7 @@ export namespace Hovers {
dateFormat: dateFormat,
line: editorLine,
markdown: true,
pr: pr,
presence: presence,
previousLineDiffUris: previousLineDiffUris,
remotes: remotes
@ -180,4 +185,15 @@ export namespace Hovers {
hunkLine.current === undefined ? '' : `\n+${hunkLine.current.line}`
}\n\`\`\``;
}
async function getPullRequestForCommit(ref: string, remotes: GitRemote[]) {
try {
return await Container.git.getPullRequestForCommit(ref, remotes, { timeout: 250 });
} catch (ex) {
if (ex instanceof Promises.CancellationError) {
return ex;
}
return undefined;
}
}
}

+ 16
- 0
src/system/promise.ts View File

@ -60,6 +60,22 @@ export namespace Promises {
);
});
}
export function first<T>(promises: Promise<T>[], predicate: (value: T) => boolean): Promise<T | undefined> {
const newPromises: Promise<T | undefined>[] = promises.map(
p =>
new Promise<T>((resolve, reject) =>
p.then(value => {
if (predicate(value)) {
resolve(value);
}
}, reject)
)
);
newPromises.push(Promise.all(promises).then(() => undefined));
return Promise.race(newPromises);
}
export function is<T>(obj: T | Promise<T>): obj is Promise<T> {
return obj != null && typeof (obj as Promise<T>).then === 'function';
}

+ 27
- 19
src/views/nodes/remoteNode.ts View File

@ -3,7 +3,7 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ViewBranchesLayout } from '../../configuration';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { GitRemote, GitRemoteType, GitUri, Repository } from '../../git/gitService';
import { GitRemote, GitRemoteType, GitUri, RemoteProviderWithApi, Repository } from '../../git/gitService';
import { Arrays, log } from '../../system';
import { RepositoriesView } from '../repositoriesView';
import { BranchNode } from './branchNode';
@ -72,7 +72,7 @@ export class RemoteNode extends ViewNode {
return children;
}
getTreeItem(): TreeItem {
async getTreeItem(): Promise<TreeItem> {
let arrows;
let left;
let right;
@ -103,35 +103,43 @@ export class RemoteNode extends ViewNode {
TreeItemCollapsibleState.Collapsed
);
item.description =
this.remote.provider !== undefined
? `${arrows}${GlyphChars.Space} ${this.remote.provider.name} ${GlyphChars.Space}${GlyphChars.Dot}${GlyphChars.Space} ${this.remote.provider.displayPath}`
: `${arrows}${GlyphChars.Space} ${
this.remote.domain
? `${this.remote.domain} ${GlyphChars.Space}${GlyphChars.Dot}${GlyphChars.Space} `
: ''
}${this.remote.path}`;
item.contextValue = ResourceType.Remote;
if (this.remote.default) {
item.contextValue += '+default';
}
if (this.remote.provider !== undefined) {
if (this.remote.provider != null) {
item.description = `${arrows}${GlyphChars.Space} ${this.remote.provider.name} ${GlyphChars.Space}${GlyphChars.Dot}${GlyphChars.Space} ${this.remote.provider.displayPath}`;
item.iconPath = {
dark: Container.context.asAbsolutePath(`images/dark/icon-${this.remote.provider.icon}.svg`),
light: Container.context.asAbsolutePath(`images/light/icon-${this.remote.provider.icon}.svg`)
};
if (this.remote.provider instanceof RemoteProviderWithApi) {
const connected = await this.remote.provider.isConnected();
item.contextValue += `${ResourceType.Remote}${connected ? '+connected' : '+disconnected'}`;
item.tooltip = `${this.remote.name} (${this.remote.provider.name} ${GlyphChars.Dash} ${
connected ? 'connected' : 'not connected'
})\n${this.remote.provider.displayPath}\n`;
} else {
item.contextValue = ResourceType.Remote;
item.tooltip = `${this.remote.name} (${this.remote.provider.name})\n${this.remote.provider.displayPath}\n`;
}
} else {
item.description = `${arrows}${GlyphChars.Space} ${
this.remote.domain
? `${this.remote.domain} ${GlyphChars.Space}${GlyphChars.Dot}${GlyphChars.Space} `
: ''
}${this.remote.path}`;
item.contextValue = ResourceType.Remote;
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/icon-remote.svg'),
light: Container.context.asAbsolutePath('images/light/icon-remote.svg')
};
item.tooltip = `${this.remote.name} (${this.remote.domain})\n${this.remote.path}\n`;
}
if (this.remote.default) {
item.contextValue += '+default';
}
item.id = this.id;
item.tooltip = `${this.remote.name} (${
this.remote.provider !== undefined ? this.remote.provider.name : this.remote.domain
})\n${this.remote.provider !== undefined ? this.remote.provider.displayPath : this.remote.path}\n`;
for (const type of this.remote.types) {
item.tooltip += `\n${type.url} (${type.type})`;

+ 27
- 2
webpack.config.js View File

@ -27,6 +27,21 @@ module.exports = function(env, argv) {
env.optimizeImages = true;
}
// TODO: Total and complete HACK until the following issue is resolved
// https://github.com/gr2m/universal-user-agent/issues/23
const packageJSON = path.resolve(__dirname, 'node_modules/universal-user-agent/package.json');
if (fs.existsSync(packageJSON)) {
// eslint-disable-next-line import/no-dynamic-require
const uua = require(packageJSON);
console.log(uua.module);
if (uua.module !== 'dist-node/index.js') {
console.log("Rewrote universal-user-agent's package.json module field to `dist-node/index.js`");
uua.module = 'dist-node/index.js';
fs.writeFileSync(packageJSON, `${JSON.stringify(uua, undefined, 4)}\n`, 'utf8');
}
}
return [getExtensionConfig(env), getWebviewsConfig(env)];
};
@ -78,7 +93,8 @@ function getExtensionConfig(env) {
devtool: 'source-map',
output: {
libraryTarget: 'commonjs2',
filename: 'extension.js'
filename: 'extension.js',
chunkFilename: 'feature-[name].js'
},
optimization: {
minimizer: [
@ -94,7 +110,13 @@ function getExtensionConfig(env) {
module: true
}
})
]
],
splitChunks: {
cacheGroups: {
vendors: false
},
chunks: 'async'
}
},
externals: {
vscode: 'commonjs vscode'
@ -116,6 +138,9 @@ function getExtensionConfig(env) {
]
},
resolve: {
// alias: {
// 'universal-user-agent': 'node_modules/universal-user-agent/dist-node/index.js'
// }
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
symlinks: false
},

+ 103
- 1
yarn.lock View File

@ -31,6 +31,54 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@octokit/endpoint@^5.5.0":
version "5.5.1"
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-5.5.1.tgz#2eea81e110ca754ff2de11c79154ccab4ae16b3f"
integrity sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==
dependencies:
"@octokit/types" "^2.0.0"
is-plain-object "^3.0.0"
universal-user-agent "^4.0.0"
"@octokit/graphql@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.3.1.tgz#9ee840e04ed2906c7d6763807632de84cdecf418"
integrity sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==
dependencies:
"@octokit/request" "^5.3.0"
"@octokit/types" "^2.0.0"
universal-user-agent "^4.0.0"
"@octokit/request-error@^1.0.1":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.0.tgz#a64d2a9d7a13555570cd79722de4a4d76371baaa"
integrity sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==
dependencies:
"@octokit/types" "^2.0.0"
deprecation "^2.0.0"
once "^1.4.0"
"@octokit/request@^5.3.0":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.3.1.tgz#3a1ace45e6f88b1be4749c5da963b3a3b4a2f120"
integrity sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==
dependencies:
"@octokit/endpoint" "^5.5.0"
"@octokit/request-error" "^1.0.1"
"@octokit/types" "^2.0.0"
deprecation "^2.0.0"
is-plain-object "^3.0.0"
node-fetch "^2.3.0"
once "^1.4.0"
universal-user-agent "^4.0.0"
"@octokit/types@^2.0.0":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.0.2.tgz#0888497f5a664e28b0449731d5e88e19b2a74f90"
integrity sha512-StASIL2lgT3TRjxv17z9pAqbnI7HGu9DrJlg3sEBFfCLaMEqp+O3IQPUF6EZtQ4xkAu2ml6kMBBCtGxjvmtmuQ==
dependencies:
"@types/node" ">= 8"
"@sindresorhus/is@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
@ -101,6 +149,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
"@types/keytar@4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.0.tgz#ca24e6ee6d0df10c003aafe26e93113b8faf0d8e"
integrity sha512-cq/NkUUy6rpWD8n7PweNQQBpw2o0cf5v6fbkUVEpOB9VzzIvyPvSEId1/goIj+MciW2v1Lw5mRimKO01XgE9EA==
"@types/lodash-es@4.17.3":
version "4.17.3"
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.3.tgz#87eb0b3673b076b8ee655f1890260a136af09a2d"
@ -118,7 +171,7 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/node@*":
"@types/node@*", "@types/node@>= 8":
version "12.12.14"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.14.tgz#1c1d6e3c75dba466e0326948d56e8bd72a1903d2"
integrity sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA==
@ -1824,6 +1877,11 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
deprecation@^2.0.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
des.js@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@ -3790,6 +3848,13 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-plain-object@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.0.tgz#47bfc5da1b5d50d64110806c199359482e75a928"
integrity sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==
dependencies:
isobject "^4.0.0"
is-png@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-png/-/is-png-1.1.0.tgz#d574b12bf275c0350455570b0e5b57ab062077ce"
@ -3873,6 +3938,11 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
isobject@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@ -4192,6 +4262,11 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
macos-release@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==
make-dir@^1.0.0, make-dir@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
@ -4607,6 +4682,11 @@ no-case@^2.2.0:
dependencies:
lower-case "^1.1.1"
node-fetch@^2.3.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-gyp@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
@ -4940,6 +5020,14 @@ os-locale@^3.1.0:
lcid "^2.0.0"
mem "^4.0.0"
os-name@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801"
integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==
dependencies:
macos-release "^2.2.0"
windows-release "^3.1.0"
os-tmpdir@^1.0.0, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@ -6889,6 +6977,13 @@ unique-slug@^2.0.0:
dependencies:
imurmurhash "^0.1.4"
universal-user-agent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.0.tgz#27da2ec87e32769619f68a14996465ea1cb9df16"
integrity sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==
dependencies:
os-name "^3.1.0"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@ -7182,6 +7277,13 @@ wide-align@^1.1.0:
dependencies:
string-width "^1.0.2 || 2"
windows-release@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f"
integrity sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==
dependencies:
execa "^1.0.0"
word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"

Loading…
Cancel
Save