Browse Source

Adds pr nodes for branches & commits

main
Eric Amodio 4 years ago
parent
commit
936f02e031
18 changed files with 667 additions and 72 deletions
  1. +161
    -28
      package.json
  2. +2
    -1
      src/commands.ts
  3. +2
    -0
      src/commands/common.ts
  4. +57
    -0
      src/commands/openPullRequestOnRemote.ts
  5. +32
    -0
      src/config.ts
  6. +59
    -2
      src/git/gitService.ts
  7. +13
    -1
      src/git/models/branch.ts
  8. +9
    -1
      src/git/models/commit.ts
  9. +5
    -0
      src/git/models/pullRequest.ts
  10. +21
    -1
      src/git/remotes/github.ts
  11. +41
    -1
      src/git/remotes/provider.ts
  12. +148
    -25
      src/github/github.ts
  13. +1
    -1
      src/statusbar/statusBarController.ts
  14. +26
    -4
      src/views/nodes/branchNode.ts
  15. +16
    -4
      src/views/nodes/commitNode.ts
  16. +70
    -0
      src/views/nodes/pullRequestNode.ts
  17. +3
    -3
      src/views/nodes/stashNode.ts
  18. +1
    -0
      src/views/nodes/viewNode.ts

+ 161
- 28
package.json View File

@ -1510,30 +1510,6 @@
"markdownDescription": "Specifies the string to be shown in place of the _authors_ code lens when there are unsaved changes",
"scope": "window"
},
"gitlens.views.commitFileFormat": {
"type": "string",
"default": "${file}",
"markdownDescription": "Specifies the format of a committed file in the views. See [_File Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#file-tokens) in the GitLens docs",
"scope": "window"
},
"gitlens.views.commitFileDescriptionFormat": {
"type": "string",
"default": "${directory}${ ← originalPath}",
"markdownDescription": "Specifies the description format of a committed file in the views. See [_File Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#file-tokens) in the GitLens docs",
"scope": "window"
},
"gitlens.views.commitFormat": {
"type": "string",
"default": "${❰ tips ❱➤ }${message}",
"markdownDescription": "Specifies the format of committed changes in the views. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs",
"scope": "window"
},
"gitlens.views.commitDescriptionFormat": {
"type": "string",
"default": "${changes • }${author}, ${agoOrDate}",
"markdownDescription": "Specifies the description format of committed changes in the views. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs",
"scope": "window"
},
"gitlens.views.branches.avatars": {
"type": "boolean",
"default": true,
@ -1582,6 +1558,48 @@
"markdownDescription": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the _Branches_ view. Only applies when `#gitlens.views.branches.files.layout#` is set to `auto`",
"scope": "window"
},
"gitlens.views.branches.pullRequests.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to query for pull requests associated with branches and commits in the _Branches_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.branches.pullRequests.showForBranches": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show pull requests (if any) associated with branches in the _Branches_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.branches.pullRequests.showForCommits": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show pull requests (if any) associated with commits in the _Branches_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.commitFileFormat": {
"type": "string",
"default": "${file}",
"markdownDescription": "Specifies the format of a committed file in the views. See [_File Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#file-tokens) in the GitLens docs",
"scope": "window"
},
"gitlens.views.commitFileDescriptionFormat": {
"type": "string",
"default": "${directory}${ ← originalPath}",
"markdownDescription": "Specifies the description format of a committed file in the views. See [_File Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#file-tokens) in the GitLens docs",
"scope": "window"
},
"gitlens.views.commitFormat": {
"type": "string",
"default": "${❰ tips ❱➤ }${message}",
"markdownDescription": "Specifies the format of committed changes in the views. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs",
"scope": "window"
},
"gitlens.views.commitDescriptionFormat": {
"type": "string",
"default": "${changes • }${author}, ${agoOrDate}",
"markdownDescription": "Specifies the description format of committed changes in the views. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs",
"scope": "window"
},
"gitlens.views.commits.avatars": {
"type": "boolean",
"default": true,
@ -1616,6 +1634,24 @@
"markdownDescription": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the _Commits_ view. Only applies when `#gitlens.views.commits.files.layout#` is set to `auto`",
"scope": "window"
},
"gitlens.views.commits.pullRequests.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to query for pull requests associated with the current branch and commits in the _Commits_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.commits.pullRequests.showForBranches": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show pull requests (if any) associated with the current branch in the _Commits_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.commits.pullRequests.showForCommits": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show pull requests (if any) associated with commits in the _Commits_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.commits.showBranchComparison": {
"anyOf": [
{
@ -1642,13 +1678,13 @@
"gitlens.views.compare.avatars": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show avatar images instead of commit (or status) icons in the _Compare_ view",
"markdownDescription": "Specifies whether to show avatar images instead of commit (or status) icons in the _Compare Commits_ view",
"scope": "window"
},
"gitlens.views.compare.files.compact": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to compact (flatten) unnecessary file nesting in the _Compare_ view. Only applies when `#gitlens.views.compare.files.layout#` is set to `tree` or `auto`",
"markdownDescription": "Specifies whether to compact (flatten) unnecessary file nesting in the _Compare Commits_ view. Only applies when `#gitlens.views.compare.files.layout#` is set to `tree` or `auto`",
"scope": "window"
},
"gitlens.views.compare.files.layout": {
@ -1664,13 +1700,25 @@
"Displays files as a list",
"Displays files as a tree"
],
"markdownDescription": "Specifies how the _Compare_ view will display files",
"markdownDescription": "Specifies how the _Compare Commits_ view will display files",
"scope": "window"
},
"gitlens.views.compare.files.threshold": {
"type": "number",
"default": 5,
"markdownDescription": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the _Compare_ view. Only applies when `#gitlens.views.compare.files.layout#` is set to `auto`",
"markdownDescription": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the _Compare Commits_ view. Only applies when `#gitlens.views.compare.files.layout#` is set to `auto`",
"scope": "window"
},
"gitlens.views.compare.pullRequests.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to query for pull requests associated with branches and commits in the _Compare Commits_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.compare.pullRequests.showForCommits": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show pull requests (if any) associated with commits in the _Compare Commits_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.contributors.avatars": {
@ -1707,6 +1755,18 @@
"markdownDescription": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the _Contributors_ view. Only applies when `#gitlens.views.contributors.files.layout#` is set to `auto`",
"scope": "window"
},
"gitlens.views.contributors.pullRequests.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to query for pull requests associated with branches and commits in the _Contributors_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.contributors.pullRequests.showForCommits": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show pull requests (if any) associated with commits in the _Contributors_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.defaultItemLimit": {
"type": "number",
"default": 10,
@ -1785,6 +1845,24 @@
"markdownDescription": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the _Remotes_ view. Only applies when `#gitlens.views.remotes.files.layout#` is set to `auto`",
"scope": "window"
},
"gitlens.views.remotes.pullRequests.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to query for pull requests associated with branches and commits in the _Remotes_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.remotes.pullRequests.showForBranches": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show pull requests (if any) associated with branches in the _Remotes_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.remotes.pullRequests.showForCommits": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show pull requests (if any) associated with commits in the _Remotes_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.repositories.autoRefresh": {
"type": "boolean",
"default": true,
@ -1863,6 +1941,24 @@
"markdownDescription": "Specifies whether to include working tree file status for each repository in the _Repositories_ view",
"scope": "window"
},
"gitlens.views.repositories.pullRequests.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to query for pull requests associated with branches and commits in the _Repositories_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.repositories.pullRequests.showForBranches": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show pull requests (if any) associated with branches in the _Repositories_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.repositories.pullRequests.showForCommits": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show pull requests (if any) associated with commits in the _Repositories_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.repositories.showBranchComparison": {
"anyOf": [
{
@ -1920,6 +2016,18 @@
"markdownDescription": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the _Search Commits_ view. Only applies when `#gitlens.views.search.files.layout#` is set to `auto`",
"scope": "window"
},
"gitlens.views.search.pullRequests.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to query for pull requests associated with commits in the _Search Commits_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.search.pullRequests.showForCommits": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show pull requests (if any) associated with commits in the _Search Commits_ view. Requires a connection to a supported remote service (e.g. GitHub)",
"scope": "window"
},
"gitlens.views.showRelativeDateMarkers": {
"type": "boolean",
"default": true,
@ -2894,6 +3002,18 @@
"category": "GitLens"
},
{
"command": "gitlens.openPullRequestOnRemote",
"title": "Open Pull Request",
"category": "GitLens",
"icon": "$(globe)"
},
{
"command": "gitlens.openAssociatedPullRequestOnRemote",
"title": "Open Associated Pull Request",
"category": "GitLens",
"icon": "$(git-pull-request)"
},
{
"command": "gitlens.openRepoInRemote",
"title": "Open Repository on Remote",
"category": "GitLens",
@ -4459,6 +4579,14 @@
"when": "gitlens:activeFileStatus =~ /blameable/ && gitlens:activeFileStatus =~ /remotes/"
},
{
"command": "gitlens.openPullRequestOnRemote",
"when": "false"
},
{
"command": "gitlens.openAssociatedPullRequestOnRemote",
"when": "gitlens:activeFileStatus =~ /blameable/ && gitlens:activeFileStatus =~ /remotes/"
},
{
"command": "gitlens.openFileInRemote",
"when": "gitlens:activeFileStatus =~ /tracked/ && gitlens:activeFileStatus =~ /remotes/"
},
@ -6729,6 +6857,11 @@
"group": "1_gitlens_actions@2"
},
{
"command": "gitlens.openPullRequestOnRemote",
"when": "viewItem =~ /gitlens:pullrequest\\b/",
"group": "inline@99"
},
{
"command": "gitlens.views.addRemote",
"when": "!gitlens:readonly && viewItem =~ /gitlens:remotes\\b/",
"group": "inline@1"

+ 2
- 1
src/commands.ts View File

@ -18,6 +18,7 @@ export * from './commands/diffWithRevision';
export * from './commands/diffWithRevisionFrom';
export * from './commands/diffWithWorking';
export * from './commands/externalDiff';
export * from './commands/gitCommands';
export * from './commands/inviteToLiveShare';
export * from './commands/openBranchesOnRemote';
export * from './commands/openBranchOnRemote';
@ -28,10 +29,10 @@ export * from './commands/openFileOnRemote';
export * from './commands/openFileAtRevision';
export * from './commands/openFileAtRevisionFrom';
export * from './commands/openOnRemote';
export * from './commands/openPullRequestOnRemote';
export * from './commands/openRepoOnRemote';
export * from './commands/openRevisionFile';
export * from './commands/openWorkingFile';
export * from './commands/gitCommands';
export * from './commands/remoteProviders';
export * from './commands/repositories';
export * from './commands/resetSuppressedWarnings';

+ 2
- 0
src/commands/common.ts View File

@ -71,6 +71,8 @@ export enum Commands {
OpenFileAtRevision = 'gitlens.openFileRevision',
OpenFileAtRevisionFrom = 'gitlens.openFileRevisionFrom',
OpenInRemote = 'gitlens.openInRemote',
OpenPullRequestOnRemote = 'gitlens.openPullRequestOnRemote',
OpenAssociatedPullRequestOnRemote = 'gitlens.openAssociatedPullRequestOnRemote',
OpenRepoInRemote = 'gitlens.openRepoInRemote',
OpenRevisionFile = 'gitlens.openRevisionFile',
OpenRevisionFileInDiffLeft = 'gitlens.openRevisionFileInDiffLeft',

+ 57
- 0
src/commands/openPullRequestOnRemote.ts View File

@ -0,0 +1,57 @@
'use strict';
import { env, Uri } from 'vscode';
import {
Command,
command,
CommandContext,
Commands,
isCommandViewContextWithCommit,
isCommandViewContextWithFileCommit,
} from './common';
import { Container } from '../container';
import { PullRequestNode } from '../views/nodes/pullRequestNode';
export interface OpenPullRequestOnRemoteCommandArgs {
ref?: string;
repoPath?: string;
pr?: { url: string };
}
@command()
export class OpenPullRequestOnRemoteCommand extends Command {
constructor() {
super([Commands.OpenPullRequestOnRemote, Commands.OpenAssociatedPullRequestOnRemote]);
}
protected preExecute(context: CommandContext, args?: OpenPullRequestOnRemoteCommandArgs) {
if (context.command === Commands.OpenPullRequestOnRemote) {
if (context.type === 'viewItem' && context.node instanceof PullRequestNode) {
args = {
...args,
pr: { url: context.node.pullRequest.url },
};
}
} else if (isCommandViewContextWithCommit(context) || isCommandViewContextWithFileCommit(context)) {
args = { ...args, ref: context.node.commit.sha, repoPath: context.node.commit.repoPath };
}
return this.execute(args);
}
async execute(args?: OpenPullRequestOnRemoteCommandArgs) {
if (args?.pr == null) {
if (args?.repoPath == null || args?.ref == null) return;
const remote = await Container.git.getRemoteWithApiProvider(args.repoPath);
if (remote?.provider == null) return;
const pr = await Container.git.getPullRequestForCommit(args.ref, remote.provider);
if (pr == null) return;
args = { ...args };
args.pr = pr;
}
void env.openExternal(Uri.parse(args.pr.url));
}
}

+ 32
- 0
src/config.ts View File

@ -460,23 +460,41 @@ export interface BranchesViewConfig {
layout: ViewBranchesLayout;
};
files: ViewsFilesConfig;
pullRequests: {
enabled: boolean;
showForBranches: boolean;
showForCommits: boolean;
};
}
export interface CommitsViewConfig {
avatars: boolean;
branches: undefined;
files: ViewsFilesConfig;
pullRequests: {
enabled: boolean;
showForBranches: boolean;
showForCommits: boolean;
};
showBranchComparison: false | ViewShowBranchComparison;
}
export interface CompareViewConfig {
avatars: boolean;
files: ViewsFilesConfig;
pullRequests: {
enabled: boolean;
showForCommits: boolean;
};
}
export interface ContributorsViewConfig {
avatars: boolean;
files: ViewsFilesConfig;
pullRequests: {
enabled: boolean;
showForCommits: boolean;
};
}
export interface FileHistoryViewConfig {
@ -494,6 +512,11 @@ export interface RemotesViewConfig {
layout: ViewBranchesLayout;
};
files: ViewsFilesConfig;
pullRequests: {
enabled: boolean;
showForBranches: boolean;
showForCommits: boolean;
};
}
export interface RepositoriesViewConfig {
@ -507,12 +530,21 @@ export interface RepositoriesViewConfig {
enabled: boolean;
files: ViewsFilesConfig;
includeWorkingTree: boolean;
pullRequests: {
enabled: boolean;
showForBranches: boolean;
showForCommits: boolean;
};
showBranchComparison: false | ViewShowBranchComparison;
}
export interface SearchViewConfig {
avatars: boolean;
files: ViewsFilesConfig;
pullRequests: {
enabled: boolean;
showForCommits: boolean;
};
}
export interface StashesViewConfig {

+ 59
- 2
src/git/gitService.ts View File

@ -80,6 +80,7 @@ import {
GitTreeParser,
PullRequest,
PullRequestDateFormatting,
PullRequestState,
Repository,
RepositoryChange,
RepositoryChangeEvent,
@ -2529,6 +2530,58 @@ export class GitService implements Disposable {
return GitUri.fromFile(file ?? fileName, repoPath, previousRef ?? GitRevision.deletedOrMissing);
}
async getPullRequestForBranch(
branch: string,
remote: GitRemote,
options?: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number },
): Promise<PullRequest | undefined>;
async getPullRequestForBranch(
branch: string,
provider: RemoteProviderWithApi,
options?: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number },
): Promise<PullRequest | undefined>;
@gate()
@debug<GitService['getPullRequestForBranch']>({
args: {
1: (remoteOrProvider: GitRemote | RemoteProviderWithApi) => remoteOrProvider.name,
},
})
async getPullRequestForBranch(
branch: string,
remoteOrProvider: GitRemote | RemoteProviderWithApi,
{
timeout,
...options
}: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number } = {},
): Promise<PullRequest | undefined> {
let provider;
if (GitRemote.is(remoteOrProvider)) {
({ provider } = remoteOrProvider);
if (!provider?.hasApi()) return undefined;
} else {
provider = remoteOrProvider;
}
let promiseOrPR = provider.getPullRequestForBranch(branch, options);
if (promiseOrPR == null || !Promises.is(promiseOrPR)) {
return promiseOrPR;
}
if (timeout != null && timeout > 0) {
promiseOrPR = Promises.cancellable(promiseOrPR, timeout);
}
try {
return await promiseOrPR;
} catch (ex) {
if (ex instanceof Promises.CancellationError) {
throw ex;
}
return undefined;
}
}
async getPullRequestForCommit(
ref: string,
remote: GitRemote,
@ -2540,7 +2593,11 @@ export class GitService implements Disposable {
options?: { timeout?: number },
): Promise<PullRequest | undefined>;
@gate()
@debug({ args: { 1: () => false } })
@debug({
args: {
1: (remoteOrProvider: GitRemote | RemoteProviderWithApi) => remoteOrProvider.name,
},
})
async getPullRequestForCommit(
ref: string,
remoteOrProvider: GitRemote | RemoteProviderWithApi,
@ -2640,7 +2697,7 @@ export class GitService implements Disposable {
remotes: GitRemote[],
options?: { includeDisconnected?: boolean },
): Promise<GitRemote<RemoteProviderWithApi> | undefined>;
@log()
@log({ args: { 0: () => false } })
async getRemoteWithApiProvider(
remotesOrRepoPath: GitRemote[] | string | undefined,
{ includeDisconnected }: { includeDisconnected?: boolean } = {},

+ 13
- 1
src/git/models/branch.ts View File

@ -4,7 +4,7 @@ import { Container } from '../../container';
import { GitRemote, GitRevision } from '../git';
import { GitStatus } from './status';
import { Dates, memoize } from '../../system';
import { GitBranchReference, GitReference } from './models';
import { GitBranchReference, GitReference, PullRequest, PullRequestState } from './models';
import { BranchSorting, configuration, DateStyle } from '../../configuration';
const whitespaceRegex = /\s/;
@ -141,6 +141,18 @@ export class GitBranch implements GitBranchReference {
return this.dateFormatter?.fromNow() ?? '';
}
async getAssociatedPullRequest(options?: {
avatarSize?: number;
include?: PullRequestState[];
limit?: number;
timeout?: number;
}): Promise<PullRequest | undefined> {
const remote = await this.getRemote();
if (remote == null) return undefined;
return Container.git.getPullRequestForBranch(this.getNameWithoutRemote(), remote, options);
}
@memoize()
getBasename(): string {
const name = this.getNameWithoutRemote();

+ 9
- 1
src/git/models/commit.ts View File

@ -6,7 +6,7 @@ import { Dates, memoize } from '../../system';
import { CommitFormatter } from '../formatters/formatters';
import { GitUri } from '../gitUri';
import { getAvatarUri } from '../../avatars';
import { GitReference, GitRevision, GitRevisionReference } from './models';
import { GitReference, GitRevision, GitRevisionReference, PullRequest } from './models';
export interface GitAuthor {
name: string;
@ -142,6 +142,14 @@ export abstract class GitCommit implements GitRevisionReference {
return GitUri.resolveToUri(this.fileName, this.repoPath);
}
@memoize()
async getAssociatedPullRequest(): Promise<PullRequest | undefined> {
const remote = await Container.git.getRemoteWithApiProvider(this.repoPath);
if (remote?.provider == null) return undefined;
return Container.git.getPullRequestForCommit(this.ref, remote);
}
@memoize<GitCommit['getPreviousLineDiffUris']>(
(uri, editorLine, ref) => `${uri.toString(true)}|${editorLine ?? ''}|${ref ?? ''}`,
)

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

@ -25,6 +25,11 @@ export class PullRequest {
constructor(
public readonly provider: string,
public readonly author: {
readonly name: string;
readonly avatarUrl: string;
readonly url: string;
},
public readonly number: number,
public readonly title: string,
public readonly url: string,

+ 21
- 1
src/git/remotes/github.ts View File

@ -3,9 +3,10 @@ import { AuthenticationSession, Range, Uri } from 'vscode';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
import { AutolinkReference } from '../../config';
import { Container } from '../../container';
import { GitHubPullRequest } from '../../github/github';
import { IssueOrPullRequest } from '../models/issue';
import { GitRevision } from '../models/models';
import { PullRequest } from '../models/pullRequest';
import { PullRequest, PullRequestState } from '../models/pullRequest';
import { Repository } from '../models/repository';
import { RemoteProviderWithApi } from './provider';
@ -164,6 +165,25 @@ export class GitHubRemote extends RemoteProviderWithApi {
});
}
protected async onGetPullRequestForBranch(
{ accessToken }: AuthenticationSession,
branch: string,
options?: {
avatarSize?: number;
include?: PullRequestState[];
limit?: number;
},
): Promise<PullRequest | undefined> {
const [owner, repo] = this.splitPath();
const { include, ...opts } = options ?? {};
return (await Container.github)?.getPullRequestForBranch(this.name, accessToken, owner, repo, branch, {
...opts,
include: include?.map(s => GitHubPullRequest.toState(s)),
baseUrl: this.apiBaseUrl,
});
}
protected async onGetPullRequestForCommit(
{ accessToken }: AuthenticationSession,
ref: string,

+ 41
- 1
src/git/remotes/provider.ts View File

@ -17,7 +17,7 @@ import { Logger } from '../../logger';
import { Messages } from '../../messages';
import { IssueOrPullRequest } from '../models/issue';
import { GitLogCommit } from '../models/logCommit';
import { PullRequest } from '../models/pullRequest';
import { PullRequest, PullRequestState } from '../models/pullRequest';
import { Repository } from '../models/repository';
import { debug, gate, Promises } from '../../system';
@ -300,6 +300,35 @@ export abstract class RemoteProviderWithApi extends RemoteProvider {
}
}
@gate()
@debug()
async getPullRequestForBranch(
branch: string,
options?: {
avatarSize?: number;
include?: PullRequestState[];
limit?: number;
},
): Promise<PullRequest | undefined> {
const cc = Logger.getCorrelationContext();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
try {
const pr = await this.onGetPullRequestForBranch(this._session!, branch, options);
this.invalidAuthenticationCount = 0;
return pr;
} catch (ex) {
Logger.error(ex, cc);
if (ex instanceof AuthenticationError) {
this.handleAuthenticationException();
}
return undefined;
}
}
private _prsByCommit = new Map<string, Promise<PullRequest | null> | PullRequest | null>();
@gate()
@ -321,6 +350,17 @@ export abstract class RemoteProviderWithApi extends RemoteProvider {
session: AuthenticationSession,
id: string,
): Promise<IssueOrPullRequest | undefined>;
protected abstract onGetPullRequestForBranch(
session: AuthenticationSession,
branch: string,
options?: {
avatarSize?: number;
include?: PullRequestState[];
limit?: number;
},
): Promise<PullRequest | undefined>;
protected abstract onGetPullRequestForCommit(
session: AuthenticationSession,
ref: string,

+ 148
- 25
src/github/github.ts View File

@ -10,6 +10,95 @@ export class GitHubApi {
1: _ => '<token>',
},
})
async getPullRequestForBranch(
provider: string,
token: string,
owner: string,
repo: string,
branch: string,
options?: {
baseUrl?: string;
avatarSize?: number;
include?: GitHubPullRequestState[];
limit?: number;
},
): Promise<PullRequest | undefined> {
const cc = Logger.getCorrelationContext();
try {
const query = `query pr($owner: String!, $repo: String!, $branch: String!, $limit: Int!, $states: [PullRequestState!], $avatarSize: Int) {
repository(name: $repo, owner: $owner) {
refs(query: $branch, refPrefix: "refs/heads/", first: 1) {
nodes {
associatedPullRequests(first: $limit, orderBy: {field: UPDATED_AT, direction: DESC}, states: $states) {
nodes {
author {
login
avatarUrl(size: $avatarSize)
url
}
permalink
number
title
state
updatedAt
closedAt
mergedAt
repository {
owner {
login
}
}
}
}
}
}
}
}`;
const rsp = await graphql<{
repository:
| {
refs: {
nodes: {
associatedPullRequests?: {
nodes?: GitHubPullRequest[];
};
}[];
};
}
| null
| undefined;
}>(query, {
owner: owner,
repo: repo,
branch: branch,
headers: { authorization: `Bearer ${token}` },
...options,
limit: options?.limit ?? 1,
});
const pr = rsp?.repository?.refs.nodes[0]?.associatedPullRequests?.nodes?.[0];
if (pr == null) return undefined;
// GitHub seems to sometimes return PRs for forks
if (pr.repository.owner.login !== owner) return undefined;
return GitHubPullRequest.from(pr, provider);
} catch (ex) {
Logger.error(ex, cc);
if (ex.code === 401) {
throw new AuthenticationError(ex);
}
throw ex;
}
}
@debug({
args: {
1: _ => '<token>',
},
})
async getPullRequestForCommit(
provider: string,
token: string,
@ -23,12 +112,17 @@ export class GitHubApi {
const cc = Logger.getCorrelationContext();
try {
const query = `query pr($owner: String!, $repo: String!, $sha: String!) {
const query = `query pr($owner: String!, $repo: String!, $ref: GitObjectID!, $avatarSize: Int) {
repository(name: $repo, owner: $owner) {
object(expression: $sha) {
object(oid: $ref) {
... on Commit {
associatedPullRequests(first: 1, orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
author {
login
avatarUrl(size: $avatarSize)
url
}
permalink
number
title
@ -49,17 +143,20 @@ export class GitHubApi {
}`;
const rsp = await graphql<{
repository?: {
object?: {
associatedPullRequests?: {
nodes?: GitHubPullRequest[];
};
};
};
repository:
| {
object?: {
associatedPullRequests?: {
nodes?: GitHubPullRequest[];
};
};
}
| null
| undefined;
}>(query, {
owner: owner,
repo: repo,
sha: ref,
ref: ref,
headers: { authorization: `Bearer ${token}` },
...options,
});
@ -69,20 +166,7 @@ export class GitHubApi {
// GitHub seems to sometimes return PRs for forks
if (pr.repository.owner.login !== owner) return undefined;
return new PullRequest(
provider,
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),
);
return GitHubPullRequest.from(pr, provider);
} catch (ex) {
Logger.error(ex, cc);
@ -171,11 +255,18 @@ interface GitHubIssueOrPullRequest {
title: string;
}
type GitHubPullRequestState = 'OPEN' | 'CLOSED' | 'MERGED';
interface GitHubPullRequest {
author: {
login: string;
avatarUrl: string;
url: string;
};
permalink: string;
number: number;
title: string;
state: 'OPEN' | 'CLOSED' | 'MERGED';
state: GitHubPullRequestState;
updatedAt: string;
closedAt: string | null;
mergedAt: string | null;
@ -185,3 +276,35 @@ interface GitHubPullRequest {
};
};
}
export namespace GitHubPullRequest {
export function from(pr: GitHubPullRequest, provider: string): PullRequest {
return new PullRequest(
provider,
{
name: pr.author.login,
avatarUrl: pr.author.avatarUrl,
url: pr.author.url,
},
pr.number,
pr.title,
pr.permalink,
fromState(pr.state),
new Date(pr.updatedAt),
pr.closedAt == null ? undefined : new Date(pr.closedAt),
pr.mergedAt == null ? undefined : new Date(pr.mergedAt),
);
}
export function fromState(state: GitHubPullRequestState): PullRequestState {
return state === 'MERGED'
? PullRequestState.Merged
: state === 'CLOSED'
? PullRequestState.Closed
: PullRequestState.Open;
}
export function toState(state: PullRequestState): GitHubPullRequestState {
return state === PullRequestState.Merged ? 'MERGED' : state === PullRequestState.Closed ? 'CLOSED' : 'OPEN';
}
}

+ 1
- 1
src/statusbar/statusBarController.ts View File

@ -133,7 +133,7 @@ export class StatusBarController implements Disposable {
this._blameStatusBarItem.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, {
messageTruncateAtNewLine: true,
dateFormat: cfg.dateFormat === null ? Container.config.defaultDateFormat : cfg.dateFormat
dateFormat: cfg.dateFormat === null ? Container.config.defaultDateFormat : cfg.dateFormat,
})}`;
switch (cfg.command) {

+ 26
- 4
src/views/nodes/branchNode.ts View File

@ -9,9 +9,17 @@ import { CompareBranchNode } from './compareBranchNode';
import { ViewBranchesLayout, ViewShowBranchComparison } from '../../configuration';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { BranchDateFormatting, GitBranch, GitBranchReference, GitLog, GitRemoteType } from '../../git/git';
import {
BranchDateFormatting,
GitBranch,
GitBranchReference,
GitLog,
GitRemoteType,
PullRequestState,
} from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { insertDateMarkers } from './helpers';
import { PullRequestNode } from './pullRequestNode';
import { RemotesView } from '../remotesView';
import { RepositoriesView } from '../repositoriesView';
import { RepositoryNode } from './repositoryNode';
@ -104,6 +112,17 @@ export class BranchNode
async getChildren(): Promise<ViewNode[]> {
if (this._children == null) {
const children = [];
const [log, pr] = await Promise.all([
this.getLog(),
this.view.config.pullRequests.enabled &&
this.view.config.pullRequests.showForBranches &&
(this.branch.tracking || this.branch.remote)
? this.branch.getAssociatedPullRequest(this.root ? { include: [PullRequestState.Open] } : undefined)
: undefined,
]);
if (log == null) return [new MessageNode(this.view, this, 'No commits could be found.')];
if (this.options.showTracking) {
const status = {
ref: this.branch.ref,
@ -136,14 +155,17 @@ export class BranchNode
);
}
if (pr != null) {
children.push(new PullRequestNode(this.view, this, pr, this.branch));
}
if (this.options.showComparison !== false && this.view instanceof CommitsView) {
children.push(new CompareBranchNode(this.uri, this.view, this, this.branch));
}
} else if (pr != null) {
children.push(new PullRequestNode(this.view, this, pr, this.branch));
}
const log = await this.getLog();
if (log == null) return [new MessageNode(this.view, this, 'No commits could be found.')];
const getBranchAndTagTips = await Container.git.getBranchesAndTagsTipsFn(
this.uri.repoPath,
this.branch.name,

+ 16
- 4
src/views/nodes/commitNode.ts View File

@ -8,10 +8,12 @@ import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { FileNode, FolderNode } from './folderNode';
import { CommitFormatter, GitBranch, GitLogCommit, GitRevisionReference } from '../../git/git';
import { PullRequestNode } from './pullRequestNode';
import { StashesView } from '../stashesView';
import { Arrays, Strings } from '../../system';
import { ViewsWithFiles } from '../viewBase';
import { ContextValues, ViewNode, ViewRefNode } from './viewNode';
import { TagsView } from '../tagsView';
export class CommitNode extends ViewRefNode<ViewsWithFiles, GitRevisionReference> {
constructor(
@ -54,16 +56,16 @@ export class CommitNode extends ViewRefNode
);
}
getChildren(): ViewNode[] {
async getChildren(): Promise<ViewNode[]> {
const commit = this.commit;
let children: FileNode[] = commit.files.map(
let children: (PullRequestNode | FileNode)[] = commit.files.map(
s => new CommitFileNode(this.view, this, s, commit.toFileCommit(s)!),
);
if (this.view.config.files.layout !== ViewFilesLayout.List) {
const hierarchy = Arrays.makeHierarchical(
children,
children as FileNode[],
n => n.uri.relativePath.split('/'),
(...parts: string[]) => Strings.normalizePath(paths.join(...parts)),
this.view.config.files.compact,
@ -72,10 +74,20 @@ export class CommitNode extends ViewRefNode
const root = new FolderNode(this.view, this, this.repoPath, '', hierarchy);
children = root.getChildren() as FileNode[];
} else {
children.sort((a, b) =>
(children as FileNode[]).sort((a, b) =>
a.label!.localeCompare(b.label!, undefined, { numeric: true, sensitivity: 'base' }),
);
}
if (!(this.view instanceof StashesView) && !(this.view instanceof TagsView)) {
if (this.view.config.pullRequests.enabled && this.view.config.pullRequests.showForCommits) {
const pr = await commit.getAssociatedPullRequest();
if (pr != null) {
children.splice(0, 0, new PullRequestNode(this.view, this, pr, commit));
}
}
}
return children;
}

+ 70
- 0
src/views/nodes/pullRequestNode.ts View File

@ -0,0 +1,70 @@
'use strict';
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchesView } from '../branchesView';
import { CommitsView } from '../commitsView';
import { CompareView } from '../compareView';
import { ContributorsView } from '../contributorsView';
import { GitBranch, GitCommit, PullRequest, PullRequestState } from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { RemotesView } from '../remotesView';
import { RepositoriesView } from '../repositoriesView';
import { RepositoryNode } from './repositoryNode';
import { SearchView } from '../searchView';
import { ContextValues, ViewNode } from './viewNode';
export class PullRequestNode extends ViewNode<
BranchesView | CommitsView | CompareView | ContributorsView | RemotesView | RepositoriesView | SearchView
> {
static key = ':pullrequest';
static getId(repoPath: string, number: number, ref: string): string {
return `${RepositoryNode.getId(repoPath)}${this.key}(${number}):${ref}`;
}
constructor(
view: BranchesView | CommitsView | CompareView | ContributorsView | RemotesView | RepositoriesView | SearchView,
parent: ViewNode,
public readonly pullRequest: PullRequest,
public readonly branchOrCommit: GitBranch | GitCommit,
) {
super(GitUri.fromRepoPath(branchOrCommit.repoPath), view, parent);
}
toClipboard(): string {
return this.pullRequest.url;
}
get id(): string {
return PullRequestNode.getId(this.branchOrCommit.repoPath, this.pullRequest.number, this.branchOrCommit.ref);
}
getChildren(): ViewNode[] {
return [];
}
getTreeItem(): TreeItem {
const item = new TreeItem(
`#${this.pullRequest.number}: ${this.pullRequest.title}`,
TreeItemCollapsibleState.None,
);
item.contextValue = ContextValues.PullRequest;
item.description = `${this.pullRequest.state}, ${this.pullRequest.formatDateFromNow()}`;
item.iconPath = new ThemeIcon('git-pull-request');
item.id = this.id;
item.tooltip = `${this.pullRequest.title}\n#${this.pullRequest.number} by ${this.pullRequest.author.name} was ${
this.pullRequest.state === PullRequestState.Open ? 'opened' : this.pullRequest.state.toLowerCase()
} ${this.pullRequest.formatDateFromNow()}`;
if (this.branchOrCommit instanceof GitCommit) {
item.tooltip = `Commit ${this.branchOrCommit.shortSha} was introduced by PR #${this.pullRequest.number}\n${item.tooltip}`;
}
// item.tooltip = `Open Pull Request #${this.pullRequest.number} on ${this.pullRequest.provider}`;
// item.command = {
// title: 'Open Pull Request',
// command: Commands.OpenPullRequestOnRemote,
// arguments: [this],
// };
return item;
}
}

+ 3
- 3
src/views/nodes/stashNode.ts View File

@ -60,20 +60,20 @@ export class StashNode extends ViewRefNode {
const item = new TreeItem(
CommitFormatter.fromTemplate(this.view.config.stashFormat, this.commit, {
messageTruncateAtNewLine: true,
dateFormat: Container.config.defaultDateFormat
dateFormat: Container.config.defaultDateFormat,
}),
TreeItemCollapsibleState.Collapsed,
);
item.id = this.id;
item.description = CommitFormatter.fromTemplate(this.view.config.stashDescriptionFormat, this.commit, {
messageTruncateAtNewLine: true,
dateFormat: Container.config.defaultDateFormat
dateFormat: Container.config.defaultDateFormat,
});
item.contextValue = ContextValues.Stash;
// eslint-disable-next-line no-template-curly-in-string
item.tooltip = CommitFormatter.fromTemplate('${ago} (${date})\n\n${message}', this.commit, {
dateFormat: Container.config.defaultDateFormat,
messageAutolinks: true
messageAutolinks: true,
});
return item;

+ 1
- 0
src/views/nodes/viewNode.ts View File

@ -29,6 +29,7 @@ export enum ContextValues {
LineHistory = 'gitlens:history:line',
Message = 'gitlens:message',
Pager = 'gitlens:pager',
PullRequest = 'gitlens:pullrequest',
Reflog = 'gitlens:reflog',
ReflogRecord = 'gitlens:reflog-record',
Remote = 'gitlens:remote',

Loading…
Cancel
Save