Skip to content

Commit c0b9220

Browse files
committed
Add a sign in command and improve the auth flow. Fixes #188
Adds a sign in command, and also adds a "Sign in" tree node if there are no credentials when expanding the tree.
1 parent 4366763 commit c0b9220

File tree

10 files changed

+163
-48
lines changed

10 files changed

+163
-48
lines changed

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@
133133
"command": "pr.deleteLocalBranch",
134134
"title": "Delete Local Branch",
135135
"category": "GitHub Pull Requests"
136+
},
137+
{
138+
"command": "pr.signin",
139+
"title": "Sign in to GitHub",
140+
"category": "GitHub Pull Requests"
141+
},
142+
{
143+
"command": "pr.signinAndRefreshList",
144+
"title": "Sign in and Refresh",
145+
"category": "GitHub Pull Requests"
136146
}
137147
],
138148
"menus": {
@@ -176,6 +186,10 @@
176186
{
177187
"command": "pr.refreshChanges",
178188
"when": "false"
189+
},
190+
{
191+
"command": "pr.signin",
192+
"when": "config.git.enabled && github:hasGitHubRemotes"
179193
}
180194
],
181195
"view/title": [

src/authentication/vsConfiguration.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ export class VSCodeConfiguration extends Configuration {
5555
return this._hosts.get(host.toLocaleLowerCase());
5656
}
5757

58+
public removeHost(host: string): void {
59+
this._hosts.delete(host);
60+
if (host === this.host) {
61+
super.update(undefined, undefined, false);
62+
}
63+
this.saveConfiguration();
64+
}
65+
5866
public update(username: string | undefined, token: string | undefined, raiseEvent: boolean = true): void {
5967
super.update(username, token, raiseEvent);
6068
this.saveConfiguration();

src/commands.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,14 @@ export function registerCommands(context: vscode.ExtensionContext, prManager: IP
137137
});
138138
return vscode.commands.executeCommand('vscode.diff', previousFileUri, fileChange.filePath, `${fileChange.fileName} from ${commit.substr(0, 8)}`, { preserveFocus: true });
139139
}));
140+
141+
context.subscriptions.push(vscode.commands.registerCommand('pr.signin', async () => {
142+
await prManager.authenticate();
143+
}));
144+
145+
context.subscriptions.push(vscode.commands.registerCommand('pr.signinAndRefreshList', async () => {
146+
if (await prManager.authenticate()) {
147+
vscode.commands.executeCommand('pr.refreshList');
148+
}
149+
}));
140150
}

src/common/authentication.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export class AuthenticationError extends Error {
2+
name: string;
3+
stack?: string;
4+
constructor(public message: string) {
5+
super(message);
6+
}
7+
}

src/common/remote.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export class Remote {
1616
return this.gitProtocol.repositoryName;
1717
}
1818

19+
public get normalizedHost(): string {
20+
const normalizedUri = this.gitProtocol.normalizeUri();
21+
return `${normalizedUri.scheme}://${normalizedUri.authority}`;
22+
}
23+
1924
constructor(
2025
public readonly remoteName: string,
2126
public readonly url: string,

src/github/credentials.ts

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Remote } from '../common/remote';
1212
import { VSCodeConfiguration } from '../authentication/vsConfiguration';
1313
import Logger from '../common/logger';
1414

15-
const SIGNIN_COMMAND = 'Sign in';
15+
const TRY_AGAIN = 'Try again?';
1616

1717
export class CredentialStore {
1818
private _octokits: Map<string, Octokit>;
@@ -26,28 +26,31 @@ export class CredentialStore {
2626
this._octokits = new Map<string, Octokit>();
2727
}
2828

29-
public async getOctokit(remote: Remote): Promise<Octokit> {
29+
public async hasOctokit(remote: Remote): Promise<boolean> {
3030
// the remote url might be http[s]/git/ssh but we always go through https for the api
3131
// so use a normalized http[s] url regardless of the original protocol
3232
const normalizedUri = remote.gitProtocol.normalizeUri();
3333
const host = `${normalizedUri.scheme}://${normalizedUri.authority}`;
3434

35-
// for authentication purposes only the host portion matters
3635
if (this._octokits.has(host)) {
37-
return this._octokits.get(host);
36+
return true;
3837
}
3938

4039
this._configuration.setHost(host);
4140

42-
let octokit: Octokit;
4341
const creds: IHostConfiguration = this._configuration;
4442
const server = new GitHubServer(host);
45-
let error: string;
43+
let octokit: Octokit;
4644

47-
if (creds.token && await server.validate(creds.username, creds.token)) {
48-
octokit = this.createOctokit('token', creds);
49-
} else {
45+
if (creds.token) {
46+
if (await server.validate(creds.username, creds.token)) {
47+
octokit = this.createOctokit('token', creds);
48+
} else {
49+
this._configuration.removeHost(creds.host);
50+
}
51+
}
5052

53+
if (!octokit) {
5154
// see if the system keychain has something we can use
5255
const data = await fill(host);
5356
if (data) {
@@ -57,49 +60,56 @@ export class CredentialStore {
5760
this._configuration.update(login.username, login.token, false);
5861
}
5962
}
63+
}
6064

61-
const result = await vscode.window.showInformationMessage(
62-
`In order to use the Pull Requests functionality, you need to sign in to ${normalizedUri.authority}`,
63-
SIGNIN_COMMAND);
64-
65-
if (result === SIGNIN_COMMAND) {
66-
try {
67-
const login = await server.login();
68-
if (login) {
69-
octokit = this.createOctokit('token', login)
70-
this._configuration.update(login.username, login.token, false);
71-
vscode.window.showInformationMessage(`You are now signed in to ${normalizedUri.authority}`);
72-
}
73-
} catch (e) {
74-
error = e;
75-
}
76-
}
65+
if (octokit) {
66+
this._octokits.set(host, octokit);
7767
}
7868

79-
if (!octokit) {
69+
return this._octokits.has(host);
70+
}
8071

81-
Logger.appendLine(`Error signing in to ${normalizedUri.authority}: ${error}`);
72+
public getOctokit(remote: Remote): Octokit {
73+
const normalizedUri = remote.gitProtocol.normalizeUri();
74+
const host = `${normalizedUri.scheme}://${normalizedUri.authority}`;
75+
return this._octokits.get(host);
76+
}
8277

83-
// anonymous access, not guaranteed to work for everything, and rate limited
84-
if (await server.checkAnonymousAccess()) {
85-
octokit = this.createOctokit('token', creds);
86-
if (error) {
87-
vscode.window.showWarningMessage(`Error signing in to ${normalizedUri.authority}: ${error}. Pull Requests functionality might not work correctly for this server.`)
88-
} else {
89-
vscode.window.showWarningMessage(`Not signed in to ${normalizedUri.authority}. Pull Requests functionality might not work correctly for this server.`)
90-
}
78+
public async login(remote: Remote): Promise<Octokit> {
79+
// the remote url might be http[s]/git/ssh but we always go through https for the api
80+
// so use a normalized http[s] url regardless of the original protocol
81+
const normalizedUri = remote.gitProtocol.normalizeUri();
82+
const host = `${normalizedUri.scheme}://${normalizedUri.authority}`;
9183

92-
// the server does not support anonymous access, disable everything
93-
} else {
94-
if (error) {
95-
vscode.window.showErrorMessage(`Error signing in to ${normalizedUri.authority}: ${error}`);
96-
} else {
97-
vscode.window.showWarningMessage(`Not signed in to ${normalizedUri.authority}. Pull Requests functionality is disabled for this server.`)
84+
let retry: boolean = true;
85+
let octokit: Octokit;
86+
const server = new GitHubServer(host);
87+
88+
while (retry) {
89+
let error: string;
90+
91+
try {
92+
const login = await server.login();
93+
if (login) {
94+
octokit = this.createOctokit('token', login)
95+
this._configuration.update(login.username, login.token, false);
96+
vscode.window.showInformationMessage(`You are now signed in to ${normalizedUri.authority}`);
9897
}
98+
} catch (e) {
99+
error = e;
100+
}
101+
102+
if (octokit) {
103+
retry = false;
104+
} else if (retry) {
105+
Logger.appendLine(`Error signing in to ${normalizedUri.authority}: ${error}`);
106+
retry = (await vscode.window.showErrorMessage(`Error signing in to ${normalizedUri.authority}`, TRY_AGAIN)) === TRY_AGAIN;
99107
}
100108
}
101109

102-
this._octokits.set(host, octokit);
110+
if (octokit) {
111+
this._octokits.set(host, octokit);
112+
}
103113
return octokit;
104114
}
105115

src/github/githubRepository.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,29 @@ import * as vscode from 'vscode';
77
import * as Octokit from '@octokit/rest';
88
import Logger from "../common/logger";
99
import { Remote } from "../common/remote";
10-
import { PRType } from "./interface";
10+
import { PRType, IGitHubRepository } from "./interface";
1111
import { PullRequestModel } from "./pullRequestModel";
1212
import { CredentialStore } from './credentials';
13+
import { AuthenticationError } from '../common/authentication';
1314

1415
export const PULL_REQUEST_PAGE_SIZE = 20;
16+
const SIGNIN_COMMAND = 'Sign in';
1517

1618
export interface PullRequestData {
1719
pullRequests: PullRequestModel[];
1820
hasMorePages: boolean;
1921
}
20-
export class GitHubRepository {
22+
23+
export class GitHubRepository implements IGitHubRepository {
2124
private _octokit: Octokit;
25+
private _initialized: boolean;
2226
public get octokit(): Octokit {
2327
if (this._octokit === undefined) {
24-
throw new Error("Call ensure() before accessing this property.");
28+
if (!this._initialized) {
29+
throw new Error("Call ensure() before accessing this property.");
30+
} else {
31+
throw new AuthenticationError("Not authenticated.");
32+
}
2533
}
2634
return this._octokit;
2735
}
@@ -30,10 +38,34 @@ export class GitHubRepository {
3038
}
3139

3240
async ensure(): Promise<GitHubRepository> {
33-
this._octokit = await this._credentialStore.getOctokit(this.remote);
41+
this._initialized = true;
42+
43+
if (!await this._credentialStore.hasOctokit(this.remote)) {
44+
const normalizedUri = this.remote.gitProtocol.normalizeUri();
45+
const result = await vscode.window.showInformationMessage(
46+
`In order to use the Pull Requests functionality, you need to sign in to ${normalizedUri.authority}`,
47+
SIGNIN_COMMAND);
48+
49+
if (result === SIGNIN_COMMAND) {
50+
this._octokit = await this._credentialStore.login(this.remote);
51+
}
52+
} else {
53+
this._octokit = await this._credentialStore.getOctokit(this.remote);
54+
}
55+
3456
return this;
3557
}
3658

59+
async authenticate(): Promise<boolean> {
60+
this._initialized = true;
61+
if (!await this._credentialStore.hasOctokit(this.remote)) {
62+
this._octokit = await this._credentialStore.login(this.remote);
63+
} else {
64+
this._octokit = this._credentialStore.getOctokit(this.remote);
65+
}
66+
return this._octokit !== undefined;
67+
}
68+
3769
async getDefaultBranch(): Promise<string> {
3870
try {
3971
const { octokit, remote } = await this.ensure();

src/github/interface.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ export interface IPullRequestsPagingOptions {
129129
fetchNextPage: boolean;
130130
}
131131

132+
export interface IGitHubRepository {
133+
authenticate(): Promise<boolean>;
134+
}
135+
132136
export interface IPullRequestManager {
133137
activePullRequest?: IPullRequestModel;
134138
readonly onDidChangeActivePullRequest: vscode.Event<void>;
@@ -160,6 +164,7 @@ export interface IPullRequestManager {
160164
*/
161165
fullfillPullRequestMissingInfo(pullRequest: IPullRequestModel): Promise<void>;
162166
updateRepositories(): Promise<void>;
167+
authenticate(): Promise<boolean>;
163168

164169
/**
165170
* git related APIs

src/github/pullRequestManager.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ export class PullRequestManager implements IPullRequestManager {
8787
return Promise.resolve();
8888
}
8989

90+
async authenticate(): Promise<boolean> {
91+
let ret = false;
92+
this._credentialStore.reset();
93+
for (let repository of uniqBy(this._githubRepositories, x => x.remote.normalizedHost)) {
94+
ret = await repository.authenticate() || ret;
95+
}
96+
return ret;
97+
}
98+
9099
async getLocalPullRequests(): Promise<IPullRequestModel[]> {
91100
const githubRepositories = this._githubRepositories;
92101

src/view/treeNodes/categoryNode.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import { IPullRequestManager, IPullRequestModel, PRType } from '../../github/int
99
import { PRNode } from './pullRequestNode';
1010
import { TreeNode } from './treeNode';
1111
import { formatError } from '../../common/utils';
12+
import { AuthenticationError } from '../../common/authentication';
1213

1314
export enum PRCategoryActionType {
1415
Empty,
15-
More
16+
More,
17+
Login
1618
}
1719

1820
export class PRCategoryActionNode extends TreeNode implements vscode.TreeItem {
@@ -40,6 +42,14 @@ export class PRCategoryActionNode extends TreeNode implements vscode.TreeItem {
4042
]
4143
}
4244
break;
45+
case PRCategoryActionType.Login:
46+
this.label = 'Sign in';
47+
this.command = {
48+
title: 'Sign in',
49+
command: 'pr.signinAndRefreshList',
50+
arguments: []
51+
}
52+
break;
4353
default:
4454
break;
4555
}
@@ -95,11 +105,13 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem {
95105

96106
async getChildren(): Promise<TreeNode[]> {
97107
let hasMorePages = false;
108+
let needLogin = false;
98109
if (this._type === PRType.LocalPullRequest) {
99110
try {
100111
this.prs = await this._prManager.getLocalPullRequests();
101112
} catch (e) {
102113
vscode.window.showErrorMessage(`Fetching local pull requests failed: ${formatError(e)}`);
114+
needLogin = e instanceof AuthenticationError;
103115
}
104116
} else {
105117
if (!this.fetchNextPage) {
@@ -109,6 +121,7 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem {
109121
hasMorePages = ret[1];
110122
} catch (e) {
111123
vscode.window.showErrorMessage(`Fetching pull requests failed: ${formatError(e)}`);
124+
needLogin = e instanceof AuthenticationError;
112125
}
113126
} else {
114127
try {
@@ -117,6 +130,7 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem {
117130
hasMorePages = ret[1];
118131
} catch (e) {
119132
vscode.window.showErrorMessage(`Fetching pull requests failed: ${formatError(e)}`);
133+
needLogin = e instanceof AuthenticationError;
120134
}
121135

122136
this.fetchNextPage = false;
@@ -132,7 +146,8 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem {
132146
this.childrenDisposables = nodes;
133147
return nodes;
134148
} else {
135-
let result = [new PRCategoryActionNode(PRCategoryActionType.Empty)];
149+
let category = needLogin ? PRCategoryActionType.Login : PRCategoryActionType.Empty;
150+
let result = [new PRCategoryActionNode(category)];
136151

137152
this.childrenDisposables = result;
138153
return result;

0 commit comments

Comments
 (0)