Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Commit 4331b8c

Browse files
author
Michelle Tilley
authored
Merge pull request #474 from atom/mkt-gql-login
Show login window in GH panel
2 parents 11dcc68 + a63209e commit 4331b8c

16 files changed

+688
-2
lines changed

lib/controllers/git-controller.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import PaneItem from '../views/pane-item';
1212
import Resizer from '../views/resizer';
1313
import Tabs from '../views/tabs';
1414
import Commands, {Command} from '../views/commands';
15+
import GithubController from './github-controller';
1516
import FilePatchController from './file-patch-controller';
1617
import GitPanelController from './git-panel-controller';
1718
import StatusBarTileController from './status-bar-tile-controller';
@@ -44,6 +45,7 @@ export default class GitController extends React.Component {
4445
return {
4546
gitPanelActive: this.state.gitPanelActive,
4647
panelSize: this.state.panelSize,
48+
activeTab: this.state.activeTab,
4749
};
4850
}
4951

@@ -54,7 +56,7 @@ export default class GitController extends React.Component {
5456
amending: false,
5557
gitPanelActive: !!props.savedState.gitPanelActive,
5658
panelSize: props.savedState.panelSize || 400,
57-
activeTab: 0,
59+
activeTab: props.savedState.activeTab || 0,
5860
};
5961

6062
this.repositoryStateRegistry = new ModelStateRegistry(GitController, {
@@ -154,7 +156,7 @@ export default class GitController extends React.Component {
154156
</Tabs.Panel>
155157
{this.props.githubEnabled && (
156158
<Tabs.Panel title="Hub">
157-
Hello from Hub
159+
<GithubController repository={this.props.repository} />
158160
</Tabs.Panel>
159161
)}
160162
</Tabs>
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import React from 'react';
2+
import {autobind} from 'core-decorators';
3+
4+
import RemotePrController from './remote-pr-controller';
5+
import Repository from '../models/repository';
6+
import GithubLoginModel from '../models/github-login-model';
7+
import ObserveModel from '../decorators/observe-model';
8+
import {RemotePropType} from '../prop-types';
9+
10+
class RemoteSelector extends React.Component {
11+
static propTypes = {
12+
remotes: React.PropTypes.arrayOf(RemotePropType).isRequired,
13+
currentBranch: React.PropTypes.string.isRequired,
14+
selectRemote: React.PropTypes.func.isRequired,
15+
}
16+
17+
render() {
18+
const {remotes, currentBranch, selectRemote} = this.props;
19+
return (
20+
<div className="github-RemoteSelector">
21+
<p>
22+
This repository has multiple remotes hosted at GitHub.com.
23+
Select a remote to see pull requests associated
24+
with the <strong>{currentBranch}</strong> branch.
25+
</p>
26+
<ul>
27+
{remotes.map(remote => (
28+
<li key={remote.name}>
29+
<a href="#" onClick={e => selectRemote(e, remote)}>
30+
{remote.name} ({remote.info.owner}/{remote.info.name})
31+
</a>
32+
</li>
33+
))}
34+
</ul>
35+
</div>
36+
);
37+
}
38+
}
39+
40+
41+
@ObserveModel({
42+
getModel: props => props.repository,
43+
fetchData: async repo => {
44+
let remotes = await repo.getRemotes();
45+
const currentBranch = await repo.getCurrentBranch();
46+
const selectedRemote = await repo.getConfig('atomGithub.currentRemote');
47+
remotes = remotes.map(({name, url}) => ({name, url, info: Repository.githubInfoFromRemote(url)}))
48+
.filter(remote => remote.info.githubRepo);
49+
return {remotes, currentBranch, selectedRemote};
50+
},
51+
})
52+
export default class GithubController extends React.Component {
53+
static propTypes = {
54+
repository: React.PropTypes.object,
55+
remotes: React.PropTypes.arrayOf(RemotePropType.isRequired),
56+
currentBranch: React.PropTypes.string,
57+
selectedRemote: React.PropTypes.string,
58+
}
59+
60+
static defaultProps = {
61+
remotes: null,
62+
currentBranch: '',
63+
selectedRemote: null,
64+
}
65+
66+
constructor(props, context) {
67+
super(props, context);
68+
this.loginModel = new GithubLoginModel();
69+
}
70+
71+
render() {
72+
if (!this.props.repository || !this.props.remotes) {
73+
return null;
74+
}
75+
76+
let remote = this.props.remotes.find(r => r.name === this.props.selectedRemote);
77+
let remotesAvailable = false;
78+
if (!remote && this.props.remotes.length === 1) {
79+
remote = this.props.remotes[0];
80+
} else if (!remote && this.props.remotes.length > 1) {
81+
remotesAvailable = true;
82+
}
83+
84+
return (
85+
<div className="github-GithubController">
86+
<div className="github-GithubController-content">
87+
{/* only supporting GH.com for now, hardcoded values */}
88+
{remote &&
89+
<RemotePrController
90+
instance="github.com"
91+
endpoint="https://api.github.com/"
92+
loginModel={this.loginModel}
93+
remote={remote}
94+
currentBranch={this.props.currentBranch}
95+
/>
96+
}
97+
{!remote && remotesAvailable &&
98+
<RemoteSelector
99+
remotes={this.props.remotes}
100+
currentBranch={this.props.currentBranch}
101+
selectRemote={this.handleRemoteSelect}
102+
/>
103+
}
104+
{!remote && !remotesAvailable && this.renderNoRemotes()}
105+
</div>
106+
</div>
107+
);
108+
}
109+
110+
renderNoRemotes() {
111+
return (
112+
<div className="github-GithubController-no-remotes">
113+
This repository does not have any remotes hosted at GitHub.com.
114+
</div>
115+
);
116+
}
117+
118+
componentWillUnmount() {
119+
this.loginModel.destroy();
120+
}
121+
122+
@autobind
123+
handleRemoteSelect(e, remote) {
124+
e.preventDefault();
125+
this.props.repository.setConfig('atomGithub.currentRemote', remote.name);
126+
}
127+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import {autobind} from 'core-decorators';
3+
4+
import {RemotePropType} from '../prop-types';
5+
import ObserveModel from '../decorators/observe-model';
6+
import GithubLoginView from '../views/github-login-view';
7+
8+
@ObserveModel({
9+
getModel: props => props.loginModel,
10+
fetchData: async (loginModel, {instance}) => {
11+
return {
12+
token: await loginModel.getToken(instance),
13+
};
14+
},
15+
})
16+
export default class RemotePrController extends React.Component {
17+
static propTypes = {
18+
loginModel: React.PropTypes.object.isRequired,
19+
instance: React.PropTypes.string, // string that identifies the instance, e.g. 'github.com'
20+
endpoint: React.PropTypes.string, // fully qualified URI to the API endpoint, e.g. 'https://api.github.com/'
21+
remote: RemotePropType.isRequired,
22+
token: React.PropTypes.string,
23+
currentBranch: React.PropTypes.string.isRequired,
24+
}
25+
26+
static defaultProps = {
27+
instance: 'github.com',
28+
endpoint: 'https://api.github.com/',
29+
token: null,
30+
}
31+
32+
render() {
33+
return (
34+
<div className="github-RemotePrController">
35+
{this.props.token && <span>you gots a token! {this.props.token}</span>}
36+
{!this.props.token && <GithubLoginView onLogin={this.handleLogin} />}
37+
</div>
38+
);
39+
}
40+
41+
@autobind
42+
handleLogin(token) {
43+
this.props.loginModel.setToken(this.props.instance, token);
44+
}
45+
}

lib/decorators/observe-model.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from 'react';
2+
3+
import ModelObserver from '../models/model-observer';
4+
5+
/**
6+
* Wraps a component in a HOC that watches for a model to change
7+
* and passes data to the wrapped component as props.
8+
* Utilizes `ModelObserver` to watch for model changes.
9+
*
10+
* @ObserveModel({
11+
* // getModel takes the props passed to the outer component
12+
* // and should return the model to watch; defaults to `props.model`
13+
* getModel: props => props.repository,
14+
* // fetchData takes the model instance and the props passed
15+
* // to the outer component and should return an object (or promise
16+
* // of an object) specifying the data to be passed to the
17+
* // inner component as props
18+
* fetchModel: (repo, props) => ({ stuff: repo.getStuff() }),
19+
* })
20+
* class MyComponent extends React.Component { ... }
21+
*/
22+
export default function ObserveModel(spec) {
23+
const getModel = spec.getModel || (props => props.model);
24+
const fetchData = spec.fetchData || (() => {});
25+
26+
return function(Target) {
27+
return class extends React.Component {
28+
static displayName = `ObserveModel(${Target.name})`
29+
30+
constructor(props, context) {
31+
super(props, context);
32+
this.mounted = true;
33+
this.state = {
34+
modelData: {},
35+
};
36+
37+
this.modelObserver = new ModelObserver({
38+
fetchData: model => fetchData(model, this.props),
39+
didUpdate: () => {
40+
if (this.mounted) {
41+
this.setState({modelData: this.modelObserver.getActiveModelData()});
42+
}
43+
},
44+
});
45+
}
46+
47+
componentWillMount() {
48+
this.modelObserver.setActiveModel(getModel(this.props));
49+
}
50+
51+
componentWillReceiveProps(nextProps) {
52+
this.modelObserver.setActiveModel(getModel(nextProps));
53+
}
54+
55+
render() {
56+
const data = this.state.modelData;
57+
return <Target {...data} {...this.props} />;
58+
}
59+
60+
componentWillUnmount() {
61+
this.mounted = false;
62+
this.modelObserver.destroy();
63+
}
64+
};
65+
};
66+
}

lib/git-shell-out-strategy.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,49 @@ export default class GitShellOutStrategy {
402402
return output.trim().split(LINE_ENDING_REGEX)
403403
.map(branchName => branchName.trim().replace(/^\* /, ''));
404404
}
405+
406+
async getConfig(option, {local} = {}) {
407+
let output;
408+
try {
409+
let args = ['config'];
410+
if (local) { args.push('--local'); }
411+
args = args.concat(option);
412+
output = await this.exec(args);
413+
} catch (err) {
414+
if (err.code === 1 && err.stdErr === '') {
415+
// No matching config found
416+
return null;
417+
} else {
418+
throw err;
419+
}
420+
}
421+
422+
return output.trim();
423+
}
424+
425+
setConfig(option, value, {replaceAll} = {}) {
426+
let args = ['config'];
427+
if (replaceAll) { args.push('--replace-all'); }
428+
args = args.concat(option, value);
429+
return this.exec(args);
430+
}
431+
432+
async getRemotes() {
433+
let output = await this.getConfig(['--get-regexp', '^remote..*.url$'], {local: true});
434+
if (output) {
435+
output = output.trim();
436+
if (!output.length) { return []; }
437+
return output.split('\n').map(line => {
438+
const match = line.match(/^remote\.(.*)\.url (.*)$/);
439+
return {
440+
name: match[1],
441+
url: match[2],
442+
};
443+
});
444+
} else {
445+
return [];
446+
}
447+
}
405448
}
406449

407450
function buildAddedFilePatch(filePath, contents, stats) {

lib/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ function versionMismatch() {
2727
function startPackage() {
2828
var GithubPackage = require('./github-package').default;
2929

30+
if (atom.inDevMode()) {
31+
// Let's install some devTools
32+
try {
33+
const electronDevtoolsInstaller = require('electron-devtools-installer');
34+
const installExtension = electronDevtoolsInstaller.default;
35+
installExtension(electronDevtoolsInstaller.REACT_DEVELOPER_TOOLS);
36+
} catch (_e) {
37+
// Nothing
38+
}
39+
}
40+
3041
return new GithubPackage(
3142
atom.workspace, atom.project, atom.commands, atom.notifications, atom.config,
3243
);

lib/models/github-login-model.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {getPassword, replacePassword, deletePassword} from 'keytar';
2+
3+
import {Emitter} from 'atom';
4+
5+
// TOOD: Fall back to shelling out to `security` on unsigned-macOS builds
6+
export default class GithubLoginModel {
7+
constructor() {
8+
this.emitter = new Emitter();
9+
}
10+
11+
getToken(account) {
12+
return Promise.resolve(getPassword('atom-github', account));
13+
}
14+
15+
setToken(account, token) {
16+
replacePassword('atom-github', account, token);
17+
this.didUpdate();
18+
}
19+
20+
removeToken(account) {
21+
deletePassword('atom-github', account);
22+
this.didUpdate();
23+
}
24+
25+
didUpdate() {
26+
this.emitter.emit('did-update');
27+
}
28+
29+
onDidUpdate(cb) {
30+
return this.emitter.on('did-update', cb);
31+
}
32+
33+
destroy() {
34+
this.emitter.destroy();
35+
}
36+
}

0 commit comments

Comments
 (0)