1
0
mirror of https://github.com/desktop/desktop synced 2024-06-30 22:54:41 +00:00

Merge pull request #1133 from desktop/track-email-settings

introduce Account and obsolete overloaded "User" concept
This commit is contained in:
Josh Abernathy 2017-04-10 21:22:37 -04:00 committed by GitHub
commit ee7f41f91b
45 changed files with 557 additions and 479 deletions

View File

@ -2,7 +2,8 @@ import * as OS from 'os'
import * as URL from 'url'
import * as Querystring from 'querystring'
import { v4 as guid } from 'uuid'
import { User } from '../models/user'
import { Account } from '../models/account'
import { IEmail } from '../models/email'
import { IHTTPResponse, getHeader, HTTPMethod, request, deserialize } from './http'
import { AuthenticationMode } from './2fa'
@ -81,6 +82,19 @@ export interface IAPIEmail {
readonly email: string
readonly verified: boolean
readonly primary: boolean
/**
* `null` can be returned by the API for legacy reasons. A non-null value is
* set for the primary email address currently, but in the future visibility
* may be defined for each email address.
*/
readonly visibility: 'public' | 'private' | null
}
function convertEmailAddress(email: IAPIEmail): IEmail {
return {
...email,
visibility: email.visibility || 'public',
}
}
/** Information about an issue as returned by the GitHub API. */
@ -141,11 +155,11 @@ interface IAPIMentionablesResponse {
*/
export class API {
private client: any
private user: User
private account: Account
public constructor(user: User) {
this.user = user
this.client = new Octokat({ token: user.token, rootURL: user.endpoint })
public constructor(account: Account) {
this.account = account
this.client = new Octokat({ token: account.token, rootURL: account.endpoint })
}
/**
@ -173,15 +187,16 @@ export class API {
return this.client.repos(owner, name).fetch()
}
/** Fetch the logged in user. */
public fetchUser(): Promise<IAPIUser> {
/** Fetch the logged in account. */
public fetchAccount(): Promise<IAPIUser> {
return this.client.user.fetch()
}
/** Fetch the user's emails. */
public async fetchEmails(): Promise<ReadonlyArray<IAPIEmail>> {
/** Fetch the current user's emails. */
public async fetchEmails(): Promise<ReadonlyArray<IEmail>> {
const result = await this.client.user.emails.fetch()
return result.items
const emails: ReadonlyArray<IAPIEmail> = result.items
return emails.map(convertEmailAddress)
}
/** Fetch a commit from the repository. */
@ -249,7 +264,7 @@ export class API {
}
private authenticatedRequest(method: HTTPMethod, path: string, body?: Object, customHeaders?: Object): Promise<IHTTPResponse> {
return request(this.user.endpoint, `token ${this.user.token}`, method, path, body, customHeaders)
return request(this.account.endpoint, `token ${this.account.token}`, method, path, body, customHeaders)
}
/** Get the allowed poll interval for fetching. */
@ -362,10 +377,15 @@ export async function createAuthorization(endpoint: string, login: string, passw
}
/** Fetch the user authenticated by the token. */
export async function fetchUser(endpoint: string, token: string): Promise<User> {
export async function fetchUser(endpoint: string, token: string): Promise<Account> {
const octo = new Octokat({ token, rootURL: endpoint })
const user = await octo.user.fetch()
return new User(user.login, endpoint, token, new Array<string>(), user.avatarUrl, user.id, user.name)
const response = await octo.user.emails.fetch()
const emails: ReadonlyArray<IAPIEmail> = response.items
const formattedEmails = emails.map(convertEmailAddress)
return new Account(user.login, endpoint, token, formattedEmails, user.avatarUrl, user.id, user.name)
}
/** Get metadata from the server. */
@ -470,11 +490,11 @@ export function getDotComAPIEndpoint(): string {
return 'https://api.github.com'
}
/** Get the user for the endpoint. */
export function getUserForEndpoint(users: ReadonlyArray<User>, endpoint: string): User | null {
const filteredUsers = users.filter(u => u.endpoint === endpoint)
if (filteredUsers.length) {
return filteredUsers[0]
/** Get the account for the endpoint. */
export function getAccountForEndpoint(accounts: ReadonlyArray<Account>, endpoint: string): Account | null {
const filteredAccounts = accounts.filter(a => a.endpoint === endpoint)
if (filteredAccounts.length) {
return filteredAccounts[0]
}
return null
}

View File

@ -1,4 +1,4 @@
import { User } from '../models/user'
import { Account } from '../models/account'
import { CommitIdentity } from '../models/commit-identity'
import { IDiff } from '../models/diff'
import { Repository } from '../models/repository'
@ -29,7 +29,7 @@ export type PossibleSelections = { type: SelectionType.Repository, repository: R
/** All of the shared app state. */
export interface IAppState {
readonly users: ReadonlyArray<User>
readonly accounts: ReadonlyArray<Account>
readonly repositories: ReadonlyArray<Repository | CloningRepository>
readonly selectedState: PossibleSelections | null

View File

@ -1,8 +1,8 @@
import { User } from '../models/user'
import { Account } from '../models/account'
/** Get the auth key for the user. */
export function getKeyForUser(user: User): string {
return getKeyForEndpoint(user.endpoint)
export function getKeyForAccount(account: Account): string {
return getKeyForEndpoint(account.endpoint)
}
/** Get the auth key for the endpoint. */

View File

@ -1,8 +1,8 @@
import { IRepository } from '../../models/repository'
import { IUser } from '../../models/user'
import { IAccount } from '../../models/account'
export interface IGetUsersAction {
name: 'get-users'
export interface IGetAccountsAction {
name: 'get-accounts'
}
export interface IGetRepositoriesAction {
@ -25,15 +25,15 @@ export interface IUpdateGitHubRepositoryAction {
}
/** Add a user to the app. */
export interface IAddUserAction {
readonly name: 'add-user'
readonly user: IUser
export interface IAddAccountAction {
readonly name: 'add-account'
readonly account: IAccount
}
/** Remove a user from the app. */
export interface IRemoveUserAction {
readonly name: 'remove-user'
readonly user: IUser
export interface IRemoveAccountAction {
readonly name: 'remove-account'
readonly account: IAccount
}
/** Change a repository's `missing` status. */
@ -50,7 +50,7 @@ export interface IUpdateRepositoryPathAction {
readonly path: string
}
export type Action = IGetUsersAction | IGetRepositoriesAction |
export type Action = IGetAccountsAction | IGetRepositoriesAction |
IAddRepositoriesAction | IUpdateGitHubRepositoryAction |
IRemoveRepositoriesAction | IAddUserAction | IRemoveUserAction |
IRemoveRepositoriesAction | IAddAccountAction | IRemoveAccountAction |
IUpdateRepositoryMissingAction | IUpdateRepositoryPathAction

View File

@ -14,13 +14,13 @@ import {
PossibleSelections,
SelectionType,
} from '../app-state'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { Repository } from '../../models/repository'
import { GitHubRepository } from '../../models/github-repository'
import { FileChange, WorkingDirectoryStatus, WorkingDirectoryFileChange } from '../../models/status'
import { DiffSelection, DiffSelectionType, DiffType } from '../../models/diff'
import { matchGitHubRepository } from '../../lib/repository-matching'
import { API, getUserForEndpoint, IAPIUser } from '../../lib/api'
import { API, getAccountForEndpoint, IAPIUser } from '../../lib/api'
import { caseInsensitiveCompare } from '../compare'
import { Branch, BranchType } from '../../models/branch'
import { TipState } from '../../models/tip'
@ -78,7 +78,7 @@ const commitSummaryWidthConfigKey: string = 'commit-summary-width'
export class AppStore {
private emitter = new Emitter()
private users: ReadonlyArray<User> = new Array<User>()
private accounts: ReadonlyArray<Account> = new Array<Account>()
private repositories: ReadonlyArray<Repository> = new Array<Repository>()
private selectedRepository: Repository | CloningRepository | null = null
@ -164,7 +164,7 @@ export class AppStore {
this.cloningRepositoriesStore.onDidError(e => this.emitError(e))
this.signInStore.onDidAuthenticate(user => this.emitAuthenticate(user))
this.signInStore.onDidAuthenticate(account => this.emitAuthenticate(account))
this.signInStore.onDidUpdate(() => this.emitUpdate())
this.signInStore.onDidError(error => this.emitError(error))
@ -172,8 +172,8 @@ export class AppStore {
this.emojiStore.read(rootDir).then(() => this.emitUpdate())
}
private emitAuthenticate(user: User) {
this.emitter.emit('did-authenticate', user)
private emitAuthenticate(account: Account) {
this.emitter.emit('did-authenticate', account)
}
private emitUpdate() {
@ -203,7 +203,7 @@ export class AppStore {
* Registers an event handler which will be invoked whenever
* a user has successfully completed a sign-in process.
*/
public onDidAuthenticate(fn: (user: User) => void): Disposable {
public onDidAuthenticate(fn: (account: Account) => void): Disposable {
return this.emitter.on('did-authenticate', fn)
}
@ -330,7 +330,7 @@ export class AppStore {
public getState(): IAppState {
return {
users: this.users,
accounts: this.accounts,
repositories: [
...this.repositories,
...this.cloningRepositoriesStore.repositories,
@ -388,7 +388,7 @@ export class AppStore {
private onGitStoreLoadedCommits(repository: Repository, commits: ReadonlyArray<Commit>) {
for (const commit of commits) {
this.gitHubUserStore._loadAndCacheUser(this.users, repository, commit.sha, commit.author.email)
this.gitHubUserStore._loadAndCacheUser(this.accounts, repository, commit.sha, commit.author.email)
}
}
@ -577,7 +577,7 @@ export class AppStore {
}
public async _updateIssues(repository: GitHubRepository) {
const user = getUserForEndpoint(this.users, repository.endpoint)
const user = getAccountForEndpoint(this.accounts, repository.endpoint)
if (!user) { return }
try {
@ -596,13 +596,13 @@ export class AppStore {
}
private refreshMentionables(repository: Repository) {
const user = this.getUserForRepository(repository)
if (!user) { return }
const account = this.getAccountForRepository(repository)
if (!account) { return }
const gitHubRepository = repository.gitHubRepository
if (!gitHubRepository) { return }
this.gitHubUserStore.updateMentionables(gitHubRepository, user)
this.gitHubUserStore.updateMentionables(gitHubRepository, account)
}
private startBackgroundFetching(repository: Repository) {
@ -611,33 +611,27 @@ export class AppStore {
return
}
const user = this.getUserForRepository(repository)
if (!user) { return }
const account = this.getAccountForRepository(repository)
if (!account) { return }
if (!repository.gitHubRepository) { return }
const fetcher = new BackgroundFetcher(repository, user, r => this.fetch(r, user))
const fetcher = new BackgroundFetcher(repository, account, r => this.fetch(r, account))
fetcher.start()
this.currentBackgroundFetcher = fetcher
}
/** This shouldn't be called directly. See `Dispatcher`. */
public _loadFromSharedProcess(users: ReadonlyArray<User>, repositories: ReadonlyArray<Repository>) {
this.users = users
public _loadFromSharedProcess(accounts: ReadonlyArray<Account>, repositories: ReadonlyArray<Repository>) {
this.accounts = accounts
this.repositories = repositories
this.loading = this.repositories.length === 0 && this.users.length === 0
this.loading = this.repositories.length === 0 && this.accounts.length === 0
for (const user of users) {
// In theory a user should _always_ have an array of emails (even if it's
// empty). But in practice, if the user had run old dev builds this may
// not be the case. So for now we need to guard this. We should remove
// this check in the not too distant future.
// @joshaber (August 10, 2016)
if (!user.emails) { break }
// doing this that the current user can be found by any of their email addresses
for (const account of accounts) {
const userAssociations: ReadonlyArray<IGitHubUser> = account.emails.map(email => ({ ...account, email: email.email }))
const gitUsers = user.emails.map(email => ({ ...user, email }))
for (const user of gitUsers) {
for (const user of userAssociations) {
this.gitHubUserStore.cacheUser(user)
}
}
@ -1021,10 +1015,10 @@ export class AppStore {
const gitHubRepository = updatedRepository.gitHubRepository
if (!gitHubRepository) { return updatedRepository }
const user = this.getUserForRepository(repository)
if (!user) { return updatedRepository }
const account = this.getAccountForRepository(repository)
if (!account) { return updatedRepository }
const api = new API(user)
const api = new API(account)
const apiRepo = await api.fetchRepository(gitHubRepository.owner.login, gitHubRepository.name)
return updatedRepository.withGitHubRepository(gitHubRepository.withAPI(apiRepo))
}
@ -1044,7 +1038,7 @@ export class AppStore {
const gitStore = this.getGitStore(repository)
const remote = gitStore.remote
return remote ? matchGitHubRepository(this.users, remote.url) : null
return remote ? matchGitHubRepository(this.accounts, remote.url) : null
}
/** This shouldn't be called directly. See `Dispatcher`. */
@ -1085,7 +1079,7 @@ export class AppStore {
}
/** This shouldn't be called directly. See `Dispatcher`. */
public async _deleteBranch(repository: Repository, branch: Branch, user: User | null): Promise<void> {
public async _deleteBranch(repository: Repository, branch: Branch, account: Account | null): Promise<void> {
const defaultBranch = this.getRepositoryState(repository).branchesState.defaultBranch
if (!defaultBranch) {
return Promise.reject(new Error(`No default branch!`))
@ -1094,12 +1088,12 @@ export class AppStore {
const gitStore = this.getGitStore(repository)
await gitStore.performFailableOperation(() => checkoutBranch(repository, defaultBranch.name))
await gitStore.performFailableOperation(() => deleteBranch(repository, branch, user))
await gitStore.performFailableOperation(() => deleteBranch(repository, branch, account))
return this._refreshRepository(repository)
}
public async _push(repository: Repository, user: User | null): Promise<void> {
public async _push(repository: Repository, account: Account | null): Promise<void> {
return this.withPushPull(repository, async () => {
const gitStore = this.getGitStore(repository)
const remote = gitStore.remote
@ -1124,9 +1118,9 @@ export class AppStore {
const branch = state.branchesState.tip.branch
return gitStore.performFailableOperation(() => {
const setUpstream = branch.upstream ? false : true
return pushRepo(repository, user, remote.name, branch.name, setUpstream)
return pushRepo(repository, account, remote.name, branch.name, setUpstream)
.then(() => this._refreshRepository(repository))
.then(() => this.fetch(repository, user))
.then(() => this.fetch(repository, account))
})
}
})
@ -1165,7 +1159,7 @@ export class AppStore {
}
/** This shouldn't be called directly. See `Dispatcher`. */
public async _pull(repository: Repository, user: User | null): Promise<void> {
public async _pull(repository: Repository, account: Account | null): Promise<void> {
return this.withPushPull(repository, async () => {
const gitStore = this.getGitStore(repository)
const remote = gitStore.remote
@ -1185,9 +1179,9 @@ export class AppStore {
if (state.branchesState.tip.kind === TipState.Valid) {
const branch = state.branchesState.tip.branch
return gitStore.performFailableOperation(() => pullRepo(repository, user, remote.name, branch.name))
return gitStore.performFailableOperation(() => pullRepo(repository, account, remote.name, branch.name))
.then(() => this._refreshRepository(repository))
.then(() => this.fetch(repository, user))
.then(() => this.fetch(repository, account))
}
})
}
@ -1226,15 +1220,15 @@ export class AppStore {
}
/** Get the authenticated user for the repository. */
public getUserForRepository(repository: Repository): User | null {
public getAccountForRepository(repository: Repository): Account | null {
const gitHubRepository = repository.gitHubRepository
if (!gitHubRepository) { return null }
return getUserForEndpoint(this.users, gitHubRepository.endpoint)
return getAccountForEndpoint(this.accounts, gitHubRepository.endpoint)
}
/** This shouldn't be called directly. See `Dispatcher`. */
public async _publishRepository(repository: Repository, name: string, description: string, private_: boolean, account: User, org: IAPIUser | null): Promise<void> {
public async _publishRepository(repository: Repository, name: string, description: string, private_: boolean, account: Account, org: IAPIUser | null): Promise<void> {
const api = new API(account)
const apiRepository = await api.createRepository(org, name, description, private_)
@ -1245,7 +1239,7 @@ export class AppStore {
}
/** This shouldn't be called directly. See `Dispatcher`. */
public _clone(url: string, path: string, options: { user: User | null, branch?: string }): { promise: Promise<boolean>, repository: CloningRepository } {
public _clone(url: string, path: string, options: { account: Account | null, branch?: string }): { promise: Promise<boolean>, repository: CloningRepository } {
const promise = this.cloningRepositoriesStore.clone(url, path, options)
const repository = this.cloningRepositoriesStore
.repositories
@ -1287,18 +1281,18 @@ export class AppStore {
* these actions.
*
*/
public async fetchRefspec(repository: Repository, refspec: string, user: User | null): Promise<void> {
public async fetchRefspec(repository: Repository, refspec: string, account: Account | null): Promise<void> {
const gitStore = this.getGitStore(repository)
await gitStore.fetchRefspec(user, refspec)
await gitStore.fetchRefspec(account, refspec)
return this._refreshRepository(repository)
}
/** Fetch the repository. */
public async fetch(repository: Repository, user: User | null): Promise<void> {
public async fetch(repository: Repository, account: Account | null): Promise<void> {
await this.withPushPull(repository, async () => {
const gitStore = this.getGitStore(repository)
await gitStore.fetch(user)
await gitStore.fetch(account)
await this.fastForwardBranches(repository)
})

View File

@ -1,5 +1,5 @@
import { Repository } from '../../models/repository'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { GitHubRepository } from '../../models/github-repository'
import { API } from '../api'
import { fatalError } from '../fatal-error'
@ -25,7 +25,7 @@ const SkewUpperBound = 30 * 1000
/** The class which handles doing background fetches of the repository. */
export class BackgroundFetcher {
private readonly repository: Repository
private readonly user: User
private readonly account: Account
private readonly fetch: (repository: Repository) => Promise<void>
/** The handle for our setTimeout invocation. */
@ -34,9 +34,9 @@ export class BackgroundFetcher {
/** Flag to indicate whether `stop` has been called. */
private stopped = false
public constructor(repository: Repository, user: User, fetch: (repository: Repository) => Promise<void>) {
public constructor(repository: Repository, account: Account, fetch: (repository: Repository) => Promise<void>) {
this.repository = repository
this.user = user
this.account = account
this.fetch = fetch
}
@ -88,7 +88,7 @@ export class BackgroundFetcher {
/** Get the allowed fetch interval from the server. */
private async getFetchInterval(repository: GitHubRepository): Promise<number> {
const api = new API(this.user)
const api = new API(this.account)
let interval = DefaultFetchInterval
try {

View File

@ -1,6 +1,6 @@
import { ipcRenderer, remote } from 'electron'
import { Disposable } from 'event-kit'
import { User, IUser } from '../../models/user'
import { Account, IAccount } from '../../models/account'
import { Repository, IRepository } from '../../models/repository'
import { WorkingDirectoryFileChange, FileChange } from '../../models/status'
import { DiffSelection } from '../../models/diff'
@ -69,7 +69,7 @@ export class Dispatcher {
this.appStore = appStore
appStore.onDidAuthenticate((user) => {
this.addUser(user)
this.addAccount(user)
})
ipcRenderer.on('shared/did-update', (event, args) => this.onSharedDidUpdate(event, args))
@ -110,16 +110,16 @@ export class Dispatcher {
}
private onSharedDidUpdate(event: Electron.IpcRendererEvent, args: any[]) {
const state: {repositories: ReadonlyArray<IRepository>, users: ReadonlyArray<IUser>} = args[0].state
const inflatedUsers = state.users.map(User.fromJSON)
const state: { repositories: ReadonlyArray<IRepository>, account: ReadonlyArray<IAccount> } = args[0].state
const inflatedAccounts = state.account.map(Account.fromJSON)
const inflatedRepositories = state.repositories.map(Repository.fromJSON)
this.appStore._loadFromSharedProcess(inflatedUsers, inflatedRepositories)
this.appStore._loadFromSharedProcess(inflatedAccounts, inflatedRepositories)
}
/** Get the users */
private async loadUsers(): Promise<ReadonlyArray<User>> {
const json = await this.dispatchToSharedProcess<ReadonlyArray<IUser>>({ name: 'get-users' })
return json.map(User.fromJSON)
private async loadUsers(): Promise<ReadonlyArray<Account>> {
const json = await this.dispatchToSharedProcess<ReadonlyArray<IAccount>>({ name: 'get-accounts' })
return json.map(Account.fromJSON)
}
/** Get the repositories the user has added to the app. */
@ -317,19 +317,19 @@ export class Dispatcher {
* Perform a function which may need authentication on a repository. This may
* first update the GitHub association for the repository.
*/
private async withAuthenticatingUser<T>(repository: Repository, fn: (repository: Repository, user: User | null) => Promise<T>): Promise<T> {
private async withAuthenticatingUser<T>(repository: Repository, fn: (repository: Repository, account: Account | null) => Promise<T>): Promise<T> {
let updatedRepository = repository
let user = this.appStore.getUserForRepository(updatedRepository)
let account = this.appStore.getAccountForRepository(updatedRepository)
// If we don't have a user association, it might be because we haven't yet
// tried to associate the repository with a GitHub repository, or that
// association is out of date. So try again before we bail on providing an
// authenticating user.
if (!user) {
if (!account) {
updatedRepository = await this.refreshGitHubRepositoryInfo(repository)
user = this.appStore.getUserForRepository(updatedRepository)
account = this.appStore.getAccountForRepository(updatedRepository)
}
return fn(updatedRepository, user)
return fn(updatedRepository, account)
}
/** Push the current branch. */
@ -361,7 +361,7 @@ export class Dispatcher {
}
/** Publish the repository to GitHub with the given properties. */
public async publishRepository(repository: Repository, name: string, description: string, private_: boolean, account: User, org: IAPIUser | null): Promise<Repository> {
public async publishRepository(repository: Repository, name: string, description: string, private_: boolean, account: Account, org: IAPIUser | null): Promise<Repository> {
await this.appStore._publishRepository(repository, name, description, private_, account, org)
return this.refreshGitHubRepositoryInfo(repository)
}
@ -402,8 +402,8 @@ export class Dispatcher {
* Clone a missing repository to the previous path, and update it's
* state in the repository list if the clone completes without error.
*/
public async cloneAgain(url: string, path: string, user: User | null): Promise<void> {
const { promise, repository } = this.appStore._clone(url, path, { user })
public async cloneAgain(url: string, path: string, account: Account | null): Promise<void> {
const { promise, repository } = this.appStore._clone(url, path, { account })
await this.selectRepository(repository)
const success = await promise
if (!success) { return }
@ -421,7 +421,7 @@ export class Dispatcher {
}
/** Clone the repository to the path. */
public async clone(url: string, path: string, options: { user: User | null, branch?: string }): Promise<Repository | null> {
public async clone(url: string, path: string, options: { account: Account | null, branch?: string }): Promise<Repository | null> {
const { promise, repository } = this.appStore._clone(url, path, options)
await this.selectRepository(repository)
const success = await promise
@ -520,14 +520,14 @@ export class Dispatcher {
return this.appStore._setCommitMessage(repository, message)
}
/** Add the user to the app. */
public async addUser(user: User): Promise<void> {
return this.dispatchToSharedProcess<void>({ name: 'add-user', user })
/** Add the account to the app. */
public async addAccount(account: Account): Promise<void> {
return this.dispatchToSharedProcess<void>({ name: 'add-account', account })
}
/** Remove the given user. */
public removeUser(user: User): Promise<void> {
return this.dispatchToSharedProcess<void>({ name: 'remove-user', user })
/** Remove the given account from the app. */
public removeAccount(account: Account): Promise<void> {
return this.dispatchToSharedProcess<void>({ name: 'remove-account', account })
}
/**

View File

@ -5,7 +5,7 @@ import { Repository } from '../../models/repository'
import { WorkingDirectoryFileChange, FileStatus } from '../../models/status'
import { Branch, BranchType } from '../../models/branch'
import { Tip, TipState } from '../../models/tip'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { Commit } from '../../models/commit'
import { IRemote } from '../../models/remote'
@ -407,20 +407,20 @@ export class GitStore {
}
/**
* Fetch, using the given user for authentication.
* Fetch, using the given account for authentication.
*
* @param user - The user to use for authentication if needed.
* @param account - The account to use for authentication if needed.
*/
public async fetch(user: User | null): Promise<void> {
public async fetch(account: Account | null): Promise<void> {
const remotes = await getRemotes(this.repository)
for (const remote of remotes) {
await this.performFailableOperation(() => fetchRepo(this.repository, user, remote.name))
await this.performFailableOperation(() => fetchRepo(this.repository, account, remote.name))
}
}
/**
* Fetch a given refspec, using the given user for authentication.
* Fetch a given refspec, using the given account for authentication.
*
* @param user - The user to use for authentication if needed.
* @param refspec - The association between a remote and local ref to use as
@ -428,13 +428,13 @@ export class GitStore {
* information on refspecs: https://www.git-scm.com/book/tr/v2/Git-Internals-The-Refspec
*
*/
public async fetchRefspec(user: User | null, refspec: string): Promise<void> {
public async fetchRefspec(account: Account | null, refspec: string): Promise<void> {
// TODO: we should favour origin here
const remotes = await getRemotes(this.repository)
for (const remote of remotes) {
await this.performFailableOperation(() => fetchRefspec(this.repository, user, remote.name, refspec))
await this.performFailableOperation(() => fetchRefspec(this.repository, account, remote.name, refspec))
}
}

View File

@ -1,8 +1,8 @@
import { Emitter, Disposable } from 'event-kit'
import { Repository } from '../../models/repository'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { GitHubRepository } from '../../models/github-repository'
import { API, getUserForEndpoint, getDotComAPIEndpoint } from '../api'
import { API, getAccountForEndpoint, getDotComAPIEndpoint } from '../api'
import { GitHubUserDatabase, IGitHubUser, IMentionableAssociation } from './github-user-database'
import { fatalError } from '../fatal-error'
@ -50,8 +50,8 @@ export class GitHubUserStore {
}
/** Update the mentionable users for the repository. */
public async updateMentionables(repository: GitHubRepository, user: User): Promise<void> {
const api = new API(user)
public async updateMentionables(repository: GitHubRepository, account: Account): Promise<void> {
const api = new API(account)
const repositoryID = repository.dbID
if (!repositoryID) {
@ -70,7 +70,7 @@ export class GitHubUserStore {
const gitHubUsers: ReadonlyArray<IGitHubUser> = response.users.map(m => ({
...m,
email: m.email || '',
endpoint: user.endpoint,
endpoint: account.endpoint,
avatarURL: m.avatar_url,
}))
@ -95,7 +95,7 @@ export class GitHubUserStore {
}
/** Not to be called externally. See `Dispatcher`. */
public async _loadAndCacheUser(users: ReadonlyArray<User>, repository: Repository, sha: string | null, email: string) {
public async _loadAndCacheUser(accounts: ReadonlyArray<Account>, repository: Repository, sha: string | null, email: string) {
const endpoint = repository.gitHubRepository ? repository.gitHubRepository.endpoint : getDotComAPIEndpoint()
const key = `${endpoint}+${email.toLowerCase()}`
if (this.requestsInFlight.has(key)) { return }
@ -105,22 +105,22 @@ export class GitHubUserStore {
return
}
const user = getUserForEndpoint(users, gitHubRepository.endpoint)
if (!user) {
const account = getAccountForEndpoint(accounts, gitHubRepository.endpoint)
if (!account) {
return
}
this.requestsInFlight.add(key)
let gitUser: IGitHubUser | null = await this.database.users.where('[endpoint+email]')
.equals([ user.endpoint, email.toLowerCase() ])
.equals([ account.endpoint, email.toLowerCase() ])
.limit(1)
.first()
// TODO: Invalidate the stored user in the db after ... some reasonable time
// period.
if (!gitUser) {
gitUser = await this.findUserWithAPI(user, gitHubRepository, sha, email)
gitUser = await this.findUserWithAPI(account, gitHubRepository, sha, email)
}
if (gitUser) {
@ -131,8 +131,8 @@ export class GitHubUserStore {
this.emitUpdate()
}
private async findUserWithAPI(user: User, repository: GitHubRepository, sha: string | null, email: string): Promise<IGitHubUser | null> {
const api = new API(user)
private async findUserWithAPI(account: Account, repository: GitHubRepository, sha: string | null, email: string): Promise<IGitHubUser | null> {
const api = new API(account)
if (sha) {
const apiCommit = await api.fetchCommit(repository.owner.login, repository.name, sha)
if (apiCommit && apiCommit.author) {
@ -140,7 +140,7 @@ export class GitHubUserStore {
email,
login: apiCommit.author.login,
avatarURL: apiCommit.author.avatarUrl,
endpoint: user.endpoint,
endpoint: account.endpoint,
name: apiCommit.author.name,
}
}
@ -152,7 +152,7 @@ export class GitHubUserStore {
email,
login: matchingUser.login,
avatarURL: matchingUser.avatarUrl,
endpoint: user.endpoint,
endpoint: account.endpoint,
name: matchingUser.name,
}
}

View File

@ -1,6 +1,6 @@
import { IssuesDatabase, IIssue } from './issues-database'
import { API, IAPIIssue } from '../api'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { GitHubRepository } from '../../models/github-repository'
import { fatalError } from '../fatal-error'
@ -38,8 +38,8 @@ export class IssuesStore {
* Fetch the issues for the repository. This will delete any issues that have
* been closed and update or add any issues that have changed or been added.
*/
public async fetchIssues(repository: GitHubRepository, user: User) {
const api = new API(user)
public async fetchIssues(repository: GitHubRepository, account: Account) {
const api = new API(account)
const lastFetchDate = this.getLastFetchDate(repository)
const now = new Date()

View File

@ -1,5 +1,5 @@
import { Emitter, Disposable } from 'event-kit'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { assertNever, fatalError } from '../fatal-error'
import { askUserToOAuth } from '../../lib/oauth'
import { validateURL, InvalidURLErrorName, InvalidProtocolErrorName } from '../../ui/lib/enterprise-validate-url'
@ -169,8 +169,8 @@ export class SignInStore {
this.emitter.emit('did-update', this.getState())
}
private emitAuthenticate(user: User) {
this.emitter.emit('did-authenticate', user)
private emitAuthenticate(account: Account) {
this.emitter.emit('did-authenticate', account)
}
private emitError(error: Error) {
@ -186,7 +186,7 @@ export class SignInStore {
* Registers an event handler which will be invoked whenever
* a user has successfully completed a sign-in process.
*/
public onDidAuthenticate(fn: (user: User) => void): Disposable {
public onDidAuthenticate(fn: (account: Account) => void): Disposable {
return this.emitter.on('did-authenticate', fn)
}
@ -365,9 +365,9 @@ export class SignInStore {
this.setState({ ...currentState, loading: true })
let user: User
let account: Account
try {
user = await askUserToOAuth(currentState.endpoint)
account = await askUserToOAuth(currentState.endpoint)
} catch (e) {
this.setState({ ...currentState, error: e, loading: false })
return
@ -378,7 +378,7 @@ export class SignInStore {
return
}
this.emitAuthenticate(user)
this.emitAuthenticate(account)
this.setState({ kind: SignInStep.Success })
}

View File

@ -1,16 +1,15 @@
import * as URL from 'url'
import { getHTMLURL, API, getDotComAPIEndpoint } from './api'
import { parseRemote, parseOwnerAndName } from './remote-parsing'
import { User } from '../models/user'
import { Account } from '../models/account'
/**
* Find the user whose endpoint has a repository with the given owner and
* Find the account whose endpoint has a repository with the given owner and
* name. This will prefer dot com over other endpoints.
*/
async function findRepositoryUser(users: ReadonlyArray<User>, owner: string, name: string): Promise<User | null> {
const hasRepository = async (user: User) => {
const api = new API(user)
async function findRepositoryAccount(accounts: ReadonlyArray<Account>, owner: string, name: string): Promise<Account | null> {
const hasRepository = async (account: Account) => {
const api = new API(account)
try {
const repository = await api.fetchRepository(owner, name)
if (repository) {
@ -24,20 +23,20 @@ async function findRepositoryUser(users: ReadonlyArray<User>, owner: string, nam
}
// Prefer .com, then try all the others.
const sortedUsers = Array.from(users).sort((u1, u2) => {
if (u1.endpoint === getDotComAPIEndpoint()) {
const sortedAccounts = Array.from(accounts).sort((a1, a2) => {
if (a1.endpoint === getDotComAPIEndpoint()) {
return -1
} else if (u2.endpoint === getDotComAPIEndpoint()) {
} else if (a2.endpoint === getDotComAPIEndpoint()) {
return 1
} else {
return 0
}
})
for (const user of sortedUsers) {
const has = await hasRepository(user)
for (const account of sortedAccounts) {
const has = await hasRepository(account)
if (has) {
return user
return account
}
}
@ -53,21 +52,21 @@ async function findRepositoryUser(users: ReadonlyArray<User>, owner: string, nam
* Will throw an error if the URL is not value or it is unable to resolve
* the remote to an existing account
*/
export async function findUserForRemote(url: string, users: ReadonlyArray<User>): Promise<User> {
export async function findAccountForRemote(url: string, accounts: ReadonlyArray<Account>): Promise<Account> {
// First try parsing it as a full URL. If that doesn't work, try parsing it
// as an owner/name shortcut. And if that fails then throw our hands in the
// air because we truly don't care.
const parsedURL = parseRemote(url)
if (parsedURL) {
const dotComUser = users.find(u => {
const htmlURL = getHTMLURL(u.endpoint)
const dotComAccount = accounts.find(a => {
const htmlURL = getHTMLURL(a.endpoint)
const parsedEndpoint = URL.parse(htmlURL)
return parsedURL.hostname === parsedEndpoint.hostname
}) || null
if (dotComUser) {
return dotComUser
if (dotComAccount) {
return dotComAccount
}
}
@ -75,9 +74,9 @@ export async function findUserForRemote(url: string, users: ReadonlyArray<User>)
if (parsedOwnerAndName) {
const owner = parsedOwnerAndName.owner
const name = parsedOwnerAndName.name
const user = await findRepositoryUser(users, owner, name)
if (user) {
return user
const account = await findRepositoryAccount(accounts, owner, name)
if (account) {
return account
}
throw new Error(`Couldn't find a repository with that owner and name.`)
}

View File

@ -1,7 +1,7 @@
import { git, envForAuthentication } from './core'
import { Repository } from '../../models/repository'
import { Branch, BranchType } from '../../models/branch'
import { User } from '../../models/user'
import { Account } from '../../models/account'
/** Create a new branch from the given start point. */
export async function createBranch(repository: Repository, name: string, startPoint: string): Promise<void> {
@ -17,7 +17,7 @@ export async function renameBranch(repository: Repository, branch: Branch, newNa
* Delete the branch. If the branch has a remote branch, it too will be
* deleted.
*/
export async function deleteBranch(repository: Repository, branch: Branch, user: User | null): Promise<true> {
export async function deleteBranch(repository: Repository, branch: Branch, account: Account | null): Promise<true> {
if (branch.type === BranchType.Local) {
await git([ 'branch', '-D', branch.name ], repository.path, 'deleteBranch')
}
@ -27,7 +27,7 @@ export async function deleteBranch(repository: Repository, branch: Branch, user:
// If the user is not authenticated, the push is going to fail
// Let this propagate and leave it to the caller to handle
if (remote) {
await git([ 'push', remote, `:${branch.nameWithoutRemote}` ], repository.path, 'deleteBranch', { env: envForAuthentication(user) })
await git([ 'push', remote, `:${branch.nameWithoutRemote}` ], repository.path, 'deleteBranch', { env: envForAuthentication(account) })
}
return true

View File

@ -1,5 +1,5 @@
import { git, envForAuthentication } from './core'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { ChildProcess } from 'child_process'
const byline = require('byline')
@ -7,14 +7,14 @@ const byline = require('byline')
/** Additional arguments to provide when cloning a repository */
export type CloneOptions = {
/** The optional identity to provide when cloning. */
readonly user: User | null
readonly account: Account | null
/** The branch to checkout after the clone has completed. */
readonly branch?: string
}
/** Clone the repository to the path. */
export async function clone(url: string, path: string, options: CloneOptions, progress: (progress: string) => void): Promise<void> {
const env = envForAuthentication(options.user)
const env = envForAuthentication(options.account)
const processCallback = (process: ChildProcess) => {
byline(process.stderr).on('data', (chunk: string) => {
progress(chunk)

View File

@ -1,5 +1,5 @@
import * as Path from 'path'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { assertNever } from '../fatal-error'
import * as GitPerf from '../../ui/lib/git-perf'
@ -208,7 +208,7 @@ function getAskPassScriptPath(): string {
}
/** Get the environment for authenticating remote operations. */
export function envForAuthentication(user: User | null): Object {
export function envForAuthentication(account: Account | null): Object {
const env = {
'DESKTOP_PATH': process.execPath,
'DESKTOP_ASKPASS_SCRIPT': getAskPassScriptPath(),
@ -218,17 +218,17 @@ export function envForAuthentication(user: User | null): Object {
'GIT_TERMINAL_PROMPT': '0',
// by setting HOME to an empty value Git won't look at ~ for any global
// configuration values. This means we won't accidentally use a
// credential.helper value if it's been set by the current user
// credential.helper value if it's been set by the current account
'HOME': '',
}
if (!user) {
if (!account) {
return env
}
return Object.assign(env, {
'DESKTOP_USERNAME': user.login,
'DESKTOP_ENDPOINT': user.endpoint,
'DESKTOP_USERNAME': account.login,
'DESKTOP_ENDPOINT': account.endpoint,
})
}

View File

@ -1,22 +1,22 @@
import { git, envForAuthentication } from './core'
import { Repository } from '../../models/repository'
import { User } from '../../models/user'
import { Account } from '../../models/account'
/** Fetch from the given remote. */
export async function fetch(repository: Repository, user: User | null, remote: string): Promise<void> {
export async function fetch(repository: Repository, account: Account | null, remote: string): Promise<void> {
const options = {
successExitCodes: new Set([ 0 ]),
env: envForAuthentication(user),
env: envForAuthentication(account),
}
await git([ 'fetch', '--prune', remote ], repository.path, 'fetch', options)
}
/** Fetch a given refspec from the given remote. */
export async function fetchRefspec(repository: Repository, user: User | null, remote: string, refspec: string): Promise<void> {
export async function fetchRefspec(repository: Repository, account: Account | null, remote: string, refspec: string): Promise<void> {
const options = {
successExitCodes: new Set([ 0, 128 ]),
env: envForAuthentication(user),
env: envForAuthentication(account),
}
await git([ 'fetch', remote, refspec ], repository.path, 'fetchRefspec', options)

View File

@ -1,12 +1,12 @@
import { git, envForAuthentication, expectedAuthenticationErrors, GitError } from './core'
import { Repository } from '../../models/repository'
import { User } from '../../models/user'
import { Account } from '../../models/account'
/** Pull from the remote to the branch. */
export async function pull(repository: Repository, user: User | null, remote: string, branch: string): Promise<void> {
export async function pull(repository: Repository, account: Account | null, remote: string, branch: string): Promise<void> {
const options = {
env: envForAuthentication(user),
env: envForAuthentication(account),
expectedErrors: expectedAuthenticationErrors(),
}

View File

@ -1,16 +1,16 @@
import { git, envForAuthentication, expectedAuthenticationErrors } from './core'
import { Repository } from '../../models/repository'
import { User } from '../../models/user'
import { Account } from '../../models/account'
/** Push from the remote to the branch, optionally setting the upstream. */
export async function push(repository: Repository, user: User | null, remote: string, branch: string, setUpstream: boolean): Promise<void> {
export async function push(repository: Repository, account: Account | null, remote: string, branch: string, setUpstream: boolean): Promise<void> {
const args = [ 'push', remote, branch ]
if (setUpstream) {
args.push('--set-upstream')
}
const options = {
env: envForAuthentication(user),
env: envForAuthentication(account),
expectedErrors: expectedAuthenticationErrors(),
}

View File

@ -1,6 +1,6 @@
import { shell } from 'electron'
import { v4 as guid } from 'uuid'
import { User } from '../models/user'
import { Account } from '../models/account'
import { fatalError } from './fatal-error'
import {
getOAuthAuthorizationURL,
@ -11,7 +11,7 @@ import {
interface IOAuthState {
readonly state: string
readonly endpoint: string
readonly resolve: (user: User) => void
readonly resolve: (account: Account) => void
readonly reject: (error: Error) => void
}
@ -30,7 +30,7 @@ export function askUserToOAuth(endpoint: string) {
// Disable the lint warning since we're storing the `resolve` and `reject`
// functions.
// tslint:disable-next-line:promise-must-complete
return new Promise<User>((resolve, reject) => {
return new Promise<Account>((resolve, reject) => {
oauthState = { state: guid(), endpoint, resolve, reject }
const oauthURL = getOAuthAuthorizationURL(endpoint, oauthState.state)
@ -42,7 +42,7 @@ export function askUserToOAuth(endpoint: string) {
* Request the authenticated using, using the code given to us by the OAuth
* callback.
*/
export async function requestAuthenticatedUser(code: string): Promise<User | null> {
export async function requestAuthenticatedUser(code: string): Promise<Account | null> {
if (!oauthState) {
return fatalError('`askUserToOAuth` must be called before requesting an authenticated user.')
}
@ -56,17 +56,17 @@ export async function requestAuthenticatedUser(code: string): Promise<User | nul
}
/**
* Resolve the current OAuth request with the given user.
* Resolve the current OAuth request with the given account.
*
* Note that this can only be called after `askUserToOAuth` has been called and
* must only be called once.
*/
export function resolveOAuthRequest(user: User) {
export function resolveOAuthRequest(account: Account) {
if (!oauthState) {
return fatalError('`askUserToOAuth` must be called before resolving an auth request.')
}
oauthState.resolve(user)
oauthState.resolve(account)
oauthState = null
}

View File

@ -1,23 +1,23 @@
import * as URL from 'url'
import { GitHubRepository } from '../models/github-repository'
import { User } from '../models/user'
import { Account } from '../models/account'
import { Owner } from '../models/owner'
import { getHTMLURL } from './api'
import { parseRemote } from './remote-parsing'
/** Try to use the list of users and a remote URL to guess a GitHub repository. */
export function matchGitHubRepository(users: ReadonlyArray<User>, remote: string): GitHubRepository | null {
for (const ix in users) {
const match = matchRemoteWithUser(users[ix], remote)
export function matchGitHubRepository(accounts: ReadonlyArray<Account>, remote: string): GitHubRepository | null {
for (const ix in accounts) {
const match = matchRemoteWithAccount(accounts[ix], remote)
if (match) { return match }
}
return null
}
function matchRemoteWithUser(user: User, remote: string): GitHubRepository | null {
const htmlURL = getHTMLURL(user.endpoint)
function matchRemoteWithAccount(account: Account, remote: string): GitHubRepository | null {
const htmlURL = getHTMLURL(account.endpoint)
const parsed = URL.parse(htmlURL)
const host = parsed.hostname
@ -27,7 +27,7 @@ function matchRemoteWithUser(user: User, remote: string): GitHubRepository | nul
const owner = parsedRemote.owner
const name = parsedRemote.name
if (parsedRemote.hostname === host && owner && name) {
return new GitHubRepository(name, new Owner(owner, user.endpoint), null)
return new GitHubRepository(name, new Owner(owner, account.endpoint), null)
}
return null

54
app/src/models/account.ts Normal file
View File

@ -0,0 +1,54 @@
import { IEmail } from './email'
/** The data-only interface for Account for transport across IPC. */
export interface IAccount {
readonly token: string
readonly login: string
readonly endpoint: string
readonly emails: ReadonlyArray<IEmail>
readonly avatarURL: string
readonly id: number
readonly name: string
}
/**
* A GitHub account, representing the user found on GitHub The Website or GitHub Enterprise.
*
* This contains a token that will be used for operations that require authentication.
*/
export class Account implements IAccount {
/** The access token used to perform operations on behalf of this account */
public readonly token: string
/** The login name for this account */
public readonly login: string
/** The server for this account - GitHub or a GitHub Enterprise instance */
public readonly endpoint: string
/** The current list of email addresses associated with the account */
public readonly emails: ReadonlyArray<IEmail>
/** The profile URL to render for this account */
public readonly avatarURL: string
/** The database id for this account */
public readonly id: number
/** The friendly name associated with this account */
public readonly name: string
/** Create a new Account from some JSON. */
public static fromJSON(obj: IAccount): Account {
return new Account(obj.login, obj.endpoint, obj.token, obj.emails, obj.avatarURL, obj.id, obj.name)
}
public constructor(login: string, endpoint: string, token: string, emails: ReadonlyArray<IEmail>, avatarURL: string, id: number, name: string) {
this.login = login
this.endpoint = endpoint
this.token = token
this.emails = emails
this.avatarURL = avatarURL
this.id = id
this.name = name
}
public withToken(token: string): Account {
return new Account(this.login, this.endpoint, token, this.emails, this.avatarURL, this.id, this.name)
}
}

11
app/src/models/avatar.ts Normal file
View File

@ -0,0 +1,11 @@
/** The minimum properties we need in order to display a user's avatar. */
export interface IAvatarUser {
/** The user's email. */
readonly email: string
/** The user's avatar URL. */
readonly avatarURL: string
/** The user's name. */
readonly name: string
}

43
app/src/models/email.ts Normal file
View File

@ -0,0 +1,43 @@
/** The data-only interface for Email for transport across IPC. */
export interface IEmail {
readonly email: string
/**
* Represents whether GitHub has confirmed the user has access to this
* email address. New users require a verified email address before
* they can sign into GitHub Desktop.
*/
readonly verified: boolean
/**
* Flag for the user's preferred email address. Other email addresses
* are provided for associating commit authors with the one GitHub account.
*/
readonly primary: boolean
/**
* Defines the privacy settings for an email address provided by the user.
* If 'private' is found, we should not use this email address anywhere.
*/
readonly visibility: 'public' | 'private'
}
/**
* An email address associated with a GitHub account.
*/
export class Email implements IEmail {
public readonly email: string
public readonly verified: boolean
public readonly primary: boolean
public readonly visibility: 'public' | 'private'
/** Create a new Email from some JSON. */
public static fromJSON(obj: IEmail): Email {
return new Email(obj.email, obj.verified, obj.primary, obj.visibility)
}
public constructor(email: string, verified: boolean = false, primary: boolean = false, visibility: 'public' | 'private') {
this.email = email
this.verified = verified
this.primary = primary
this.visibility = visibility
}
}

View File

@ -1,42 +0,0 @@
/** The data-only interface for User for transport across IPC. */
export interface IUser {
readonly token: string
readonly login: string
readonly endpoint: string
readonly emails: ReadonlyArray<string>
readonly avatarURL: string
readonly id: number
readonly name: string
}
/**
* A GitHub user.
*/
export class User implements IUser {
public readonly token: string
public readonly login: string
public readonly endpoint: string
public readonly emails: ReadonlyArray<string>
public readonly avatarURL: string
public readonly id: number
public readonly name: string
/** Create a new User from some JSON. */
public static fromJSON(obj: IUser): User {
return new User(obj.login, obj.endpoint, obj.token, obj.emails, obj.avatarURL, obj.id, obj.name)
}
public constructor(login: string, endpoint: string, token: string, emails: ReadonlyArray<string>, avatarURL: string, id: number, name: string) {
this.login = login
this.endpoint = endpoint
this.token = token
this.emails = emails
this.avatarURL = avatarURL
this.id = id
this.name = name
}
public withToken(token: string): User {
return new User(this.login, this.endpoint, token, this.emails, this.avatarURL, this.id, this.name)
}
}

View File

@ -0,0 +1,82 @@
import { IDataStore, ISecureStore } from './stores'
import { getKeyForAccount } from '../lib/auth'
import { Account, IAccount } from '../models/account'
export class AccountsStore {
private dataStore: IDataStore
private secureStore: ISecureStore
private accounts: Account[]
public constructor(dataStore: IDataStore, secureStore: ISecureStore) {
this.dataStore = dataStore
this.secureStore = secureStore
this.accounts = []
}
/**
* Get the list of accounts in the cache.
*/
public getAll(): ReadonlyArray<Account> {
return this.accounts.slice()
}
/**
* Add the account to the store.
*/
public addAccount(account: Account) {
this.secureStore.setItem(getKeyForAccount(account), account.login, account.token)
this.accounts.push(account)
this.save()
}
/**
* Remove the account from the store.
*/
public removeAccount(account: Account) {
this.secureStore.deleteItem(getKeyForAccount(account), account.login)
this.accounts = this.accounts.filter(account => account.id !== account.id)
this.save()
}
/**
* Update the users in the store by mapping over them.
*/
public async map(fn: (account: Account) => Promise<Account>) {
const accounts = new Array<Account>()
for (const account of this.accounts) {
const newAccount = await fn(account)
accounts.push(newAccount)
}
this.accounts = accounts
this.save()
}
/**
* Load the users into memory from storage.
*/
public loadFromStore() {
const raw = this.dataStore.getItem('users')
if (!raw || !raw.length) {
return
}
const rawAccounts: ReadonlyArray<IAccount> = JSON.parse(raw)
const accountsWithTokens = rawAccounts.map(account => {
const accountWithoutToken = new Account(account.login, account.endpoint, '', account.emails, account.avatarURL, account.id, account.name)
const token = this.secureStore.getItem(getKeyForAccount(accountWithoutToken), account.login)
return accountWithoutToken.withToken(token || '')
})
this.accounts = accountsWithTokens
}
private save() {
const usersWithoutTokens = this.accounts.map(account => account.withToken(''))
this.dataStore.setItem('users', JSON.stringify(usersWithoutTokens))
}
}

View File

@ -1,6 +1,6 @@
import { remote, ipcRenderer } from 'electron'
import { IMessage } from './message'
import { UsersStore } from './users-store'
import { AccountsStore } from './accounts-store'
import { RepositoriesStore } from './repositories-store'
const { BrowserWindow } = remote
@ -53,10 +53,10 @@ export function register(name: string, fn: SharedProcessFunction) {
}
/** Tell all the windows that something was updated. */
export function broadcastUpdate(usersStore: UsersStore, repositoriesStore: RepositoriesStore) {
export function broadcastUpdate(accountsStore: AccountsStore, repositoriesStore: RepositoriesStore) {
BrowserWindow.getAllWindows().forEach(async (window) => {
const repositories = await repositoriesStore.getRepositories()
const state = { users: usersStore.getUsers(), repositories }
const state = { account: accountsStore.getAll(), repositories }
window.webContents.send('shared/did-update', [ { state } ])
})
}

View File

@ -1,6 +1,6 @@
import * as TokenStore from '../shared-process/token-store'
import { UsersStore } from './users-store'
import { User } from '../models/user'
import { AccountsStore } from './accounts-store'
import { Account } from '../models/account'
import { Database } from './database'
import { RepositoriesStore } from './repositories-store'
import { Repository, IRepository } from '../models/repository'
@ -9,7 +9,8 @@ import {
IAddRepositoriesAction,
IUpdateGitHubRepositoryAction,
IRemoveRepositoriesAction,
IAddUserAction,
IAddAccountAction,
IRemoveAccountAction,
IUpdateRepositoryMissingAction,
IUpdateRepositoryPathAction,
} from '../lib/dispatcher'
@ -26,23 +27,22 @@ process.on('uncaughtException', (error: Error) => {
reportError(error, getVersion())
})
const usersStore = new UsersStore(localStorage, TokenStore)
usersStore.loadFromStore()
const accountsStore = new AccountsStore(localStorage, TokenStore)
accountsStore.loadFromStore()
const database = new Database('Database')
const repositoriesStore = new RepositoriesStore(database)
const broadcastUpdate = () => broadcastUpdate_(usersStore, repositoriesStore)
const broadcastUpdate = () => broadcastUpdate_(accountsStore, repositoriesStore)
updateUsers()
updateAccounts()
async function updateUsers() {
await usersStore.map(async (user: User) => {
const api = new API(user)
const updatedUser = await api.fetchUser()
async function updateAccounts() {
await accountsStore.map(async (account: Account) => {
const api = new API(account)
const newAccount = await api.fetchAccount()
const emails = await api.fetchEmails()
const justTheEmails = emails.map(e => e.email)
return new User(updatedUser.login, user.endpoint, user.token, justTheEmails, updatedUser.avatarUrl, updatedUser.id, updatedUser.name)
return new Account(account.login, account.endpoint, account.token, emails, newAccount.avatarUrl, newAccount.id, newAccount.name)
})
broadcastUpdate()
}
@ -61,18 +61,18 @@ register('ping', () => {
return Promise.resolve('pong')
})
register('get-users', () => {
return Promise.resolve(usersStore.getUsers())
register('get-accounts', () => {
return Promise.resolve(accountsStore.getAll())
})
register('add-user', async ({ user }: IAddUserAction) => {
usersStore.addUser(User.fromJSON(user))
await updateUsers()
register('add-account', async ({ account }: IAddAccountAction) => {
accountsStore.addAccount(Account.fromJSON(account))
await updateAccounts()
return Promise.resolve()
})
register('remove-user', async ({ user }: IAddUserAction) => {
usersStore.removeUser(User.fromJSON(user))
register('remove-account', async ({ account }: IRemoveAccountAction) => {
accountsStore.removeAccount(Account.fromJSON(account))
broadcastUpdate()
return Promise.resolve()
})

View File

@ -1,69 +0,0 @@
import { IDataStore, ISecureStore } from './stores'
import { getKeyForUser } from '../lib/auth'
import { User, IUser } from '../models/user'
export class UsersStore {
private dataStore: IDataStore
private secureStore: ISecureStore
private users: User[]
public constructor(dataStore: IDataStore, secureStore: ISecureStore) {
this.dataStore = dataStore
this.secureStore = secureStore
this.users = []
}
public getUsers(): ReadonlyArray<User> {
return this.users.slice()
}
public addUser(user: User) {
this.secureStore.setItem(getKeyForUser(user), user.login, user.token)
this.users.push(user)
this.save()
}
/** Remove the user from the store. */
public removeUser(user: User) {
this.secureStore.deleteItem(getKeyForUser(user), user.login)
this.users = this.users.filter(u => u.id !== user.id)
this.save()
}
/** Change the users in the store by mapping over them. */
public async map(fn: (user: User) => Promise<User>) {
const users = new Array<User>()
for (const user of this.users) {
const newUser = await fn(user)
users.push(newUser)
}
this.users = users
this.save()
}
public loadFromStore() {
const raw = this.dataStore.getItem('users')
if (!raw || !raw.length) {
return
}
const rawUsers: ReadonlyArray<IUser> = JSON.parse(raw)
const usersWithTokens = rawUsers.map(user => {
const userWithoutToken = new User(user.login, user.endpoint, '', user.emails, user.avatarURL, user.id, user.name)
const token = this.secureStore.getItem(getKeyForUser(userWithoutToken), user.login)
return userWithoutToken.withToken(token || '')
})
this.users = usersWithTokens
}
private save() {
const usersWithoutTokens = this.users.map(user => user.withToken(''))
this.dataStore.setItem('users', JSON.stringify(usersWithoutTokens))
}
}

View File

@ -8,9 +8,9 @@ import { ButtonGroup } from '../lib/button-group'
import { Dispatcher } from '../../lib/dispatcher'
import { getDefaultDir, setDefaultDir } from '../lib/default-dir'
import { Row } from '../lib/row'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { parseOwnerAndName, IRepositoryIdentifier } from '../../lib/remote-parsing'
import { findUserForRemote } from '../../lib/find-account'
import { findAccountForRemote } from '../../lib/find-account'
import { Dialog, DialogContent, DialogError, DialogFooter } from '../dialog'
/** The name for the error when the destination already exists. */
@ -20,8 +20,8 @@ interface ICloneRepositoryProps {
readonly dispatcher: Dispatcher
readonly onDismissed: () => void
/** The logged in users. */
readonly users: ReadonlyArray<User>
/** The logged in accounts. */
readonly accounts: ReadonlyArray<Account>
}
interface ICloneRepositoryState {
@ -167,8 +167,8 @@ export class CloneRepository extends React.Component<ICloneRepositoryProps, IClo
const path = this.state.path
try {
const user = await findUserForRemote(url, this.props.users)
this.cloneImpl(url, path, user)
const account = await findAccountForRemote(url, this.props.accounts)
this.cloneImpl(url, path, account)
} catch (error) {
this.setState({
...this.state,
@ -178,8 +178,8 @@ export class CloneRepository extends React.Component<ICloneRepositoryProps, IClo
}
}
private cloneImpl(url: string, path: string, user: User | null) {
this.props.dispatcher.clone(url, path, { user })
private cloneImpl(url: string, path: string, account: Account | null) {
this.props.dispatcher.clone(url, path, { account })
this.props.onDismissed()
setDefaultDir(Path.resolve(path, '..'))

View File

@ -26,7 +26,7 @@ import { AppMenuBar } from './app-menu'
import { findItemByAccessKey, itemIsSelectable } from '../models/app-menu'
import { UpdateAvailable } from './updates'
import { Preferences } from './preferences'
import { User } from '../models/user'
import { Account } from '../models/account'
import { TipState } from '../models/tip'
import { shouldRenderApplicationMenu } from './lib/features'
import { Merge } from './merge-branch'
@ -277,22 +277,22 @@ export class App extends React.Component<IAppProps, IAppState> {
}
private getUsernameForUpdateCheck() {
const dotComUser = this.getDotComUser()
return dotComUser ? dotComUser.login : ''
const dotComAccount = this.getDotComAccount()
return dotComAccount ? dotComAccount.login : ''
}
private getDotComUser(): User | null {
private getDotComAccount(): Account | null {
const state = this.props.appStore.getState()
const users = state.users
const dotComUser = users.find(u => u.endpoint === getDotComAPIEndpoint())
return dotComUser || null
const accounts = state.accounts
const dotComAccount = accounts.find(a => a.endpoint === getDotComAPIEndpoint())
return dotComAccount || null
}
private getEnterpriseUser(): User | null {
private getEnterpriseAccount(): Account | null {
const state = this.props.appStore.getState()
const users = state.users
const enterpriseUser = users.find(u => u.endpoint !== getDotComAPIEndpoint())
return enterpriseUser || null
const accounts = state.accounts
const enterpriseAccount = accounts.find(a => a.endpoint !== getDotComAPIEndpoint())
return enterpriseAccount || null
}
private updateBranch() {
@ -727,8 +727,8 @@ export class App extends React.Component<IAppProps, IAppState> {
case PopupType.Preferences:
return <Preferences
dispatcher={this.props.dispatcher}
dotComUser={this.getDotComUser()}
enterpriseUser={this.getEnterpriseUser()}
dotComAccount={this.getDotComAccount()}
enterpriseAccount={this.getEnterpriseAccount()}
onDismissed={this.onPopupDismissed}/>
case PopupType.MergeBranch: {
const repository = popup.repository
@ -776,7 +776,7 @@ export class App extends React.Component<IAppProps, IAppState> {
)
case PopupType.CloneRepository:
return <CloneRepository
users={this.state.users}
accounts={this.state.accounts}
onDismissed={this.onPopupDismissed}
dispatcher={this.props.dispatcher} />
case PopupType.CreateBranch: {
@ -810,7 +810,7 @@ export class App extends React.Component<IAppProps, IAppState> {
<Publish
dispatcher={this.props.dispatcher}
repository={popup.repository}
users={this.state.users}
accounts={this.state.accounts}
onDismissed={this.onPopupDismissed}
/>
)
@ -1074,7 +1074,7 @@ export class App extends React.Component<IAppProps, IAppState> {
return <CloningRepositoryView repository={selectedState.repository}
state={selectedState.state}/>
} else if (selectedState.type === SelectionType.MissingRepository) {
return <MissingRepository repository={selectedState.repository} dispatcher={this.props.dispatcher} users={this.state.users} />
return <MissingRepository repository={selectedState.repository} dispatcher={this.props.dispatcher} accounts={this.state.accounts} />
} else {
return assertNever(selectedState, `Unknown state: ${selectedState}`)
}

View File

@ -1,7 +1,8 @@
import * as React from 'react'
import { Commit } from '../../models/commit'
import { IAvatarUser } from '../../models/avatar'
import { RichText } from '../lib/rich-text'
import { Avatar, IAvatarUser } from '../lib/avatar'
import { Avatar } from '../lib/avatar'
import { RelativeTime } from '../relative-time'
interface ICommitProps {

View File

@ -18,7 +18,7 @@ import { StatsDatabase, StatsStore } from '../lib/stats'
import { IssuesDatabase, IssuesStore, SignInStore } from '../lib/dispatcher'
import { requestAuthenticatedUser, resolveOAuthRequest, rejectOAuthRequest } from '../lib/oauth'
import { defaultErrorHandler, createMissingRepositoryHandler } from '../lib/dispatcher'
import { getEndpointForRepository, getUserForEndpoint } from '../lib/api'
import { getEndpointForRepository, getAccountForEndpoint } from '../lib/api'
import { getLogger } from '../lib/logging/renderer'
import { installDevGlobals } from './install-globals'
@ -124,9 +124,9 @@ function cloneRepository(url: string, branch?: string): Promise<Repository | nul
setDefaultDir(Path.resolve(path, '..'))
const state = appStore.getState()
const user = getUserForEndpoint(state.users, getEndpointForRepository(url)) || null
const account = getAccountForEndpoint(state.accounts, getEndpointForRepository(url))
return dispatcher.clone(url, path, { user, branch })
return dispatcher.clone(url, path, { account, branch })
}
async function handleCloneInDesktopOptions(repository: Repository | null, args: IOpenRepositoryArgs): Promise<void> {

View File

@ -1,18 +1,8 @@
import * as React from 'react'
import { IAvatarUser } from '../../models/avatar'
const DefaultAvatarURL = 'https://github.com/hubot.png'
/** The minimum properties we need in order to display a user's avatar. */
export interface IAvatarUser {
/** The user's email. */
readonly email: string
/** The user's avatar URL. */
readonly avatarURL: string
/** The user's name. */
readonly name: string
}
interface IAvatarProps {
/** The user whose avatar should be displayed. */

View File

@ -2,7 +2,7 @@ import * as React from 'react'
import { Commit } from '../../models/commit'
import { getGlobalConfigValue, setGlobalConfigValue } from '../../lib/git/config'
import { CommitListItem } from '../history/commit-list-item'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { CommitIdentity } from '../../models/commit-identity'
import { Form } from '../lib/form'
import { Button } from '../lib/button'
@ -10,8 +10,8 @@ import { TextBox } from '../lib/text-box'
import { Row } from '../lib/row'
interface IConfigureGitUserProps {
/** The logged-in users. */
readonly users: ReadonlyArray<User>
/** The logged-in accounts. */
readonly accounts: ReadonlyArray<Account>
/** Called after the user has chosen to save their config. */
readonly onSave?: () => void
@ -41,7 +41,7 @@ export class ConfigureGitUser extends React.Component<IConfigureGitUserProps, IC
let name = await getGlobalConfigValue('user.name')
let email = await getGlobalConfigValue('user.email')
const user = this.props.users[0]
const user = this.props.accounts[0]
if ((!name || !name.length) && user) {
name = user.name && user.name.length
? user.name
@ -49,7 +49,7 @@ export class ConfigureGitUser extends React.Component<IConfigureGitUserProps, IC
}
if ((!email || !email.length) && user) {
email = user.emails[0]
email = user.emails[0].email
}
const avatarURL = email ? this.avatarURLForEmail(email) : null
@ -130,8 +130,8 @@ export class ConfigureGitUser extends React.Component<IConfigureGitUserProps, IC
}
private avatarURLForEmail(email: string): string | null {
const matchingUser = this.props.users.find(u => u.emails.indexOf(email) > -1)
return matchingUser ? matchingUser.avatarURL : null
const matchingAccount = this.props.accounts.find(a => a.emails.findIndex(e => e.email === email) > -1)
return matchingAccount ? matchingAccount.avatarURL : null
}
private save = async () => {

View File

@ -3,8 +3,8 @@ import * as React from 'react'
import { UiView } from './ui-view'
import { Dispatcher } from '../lib/dispatcher'
import { Repository } from '../models/repository'
import { User } from '../models/user'
import { findUserForRemote } from '../lib/find-account'
import { Account } from '../models/account'
import { findAccountForRemote } from '../lib/find-account'
import { Button } from './lib/button'
import { Row } from './lib/row'
@ -12,7 +12,7 @@ import { Row } from './lib/row'
interface IMissingRepositoryProps {
readonly dispatcher: Dispatcher
readonly repository: Repository
readonly users: ReadonlyArray<User>
readonly accounts: ReadonlyArray<Account>
}
/** The view displayed when a repository is missing. */
@ -74,7 +74,7 @@ export class MissingRepository extends React.Component<IMissingRepositoryProps,
if (!cloneURL) { return }
try {
const user = await findUserForRemote(cloneURL, this.props.users)
const user = await findAccountForRemote(cloneURL, this.props.accounts)
await this.props.dispatcher.cloneAgain(cloneURL, this.props.repository.path, user)
} catch (error) {
this.props.dispatcher.postError(error)

View File

@ -1,18 +1,19 @@
import * as React from 'react'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { IAvatarUser } from '../../models/avatar'
import { Button } from '../lib/button'
import { Row } from '../lib/row'
import { assertNever } from '../../lib/fatal-error'
import { DialogContent } from '../dialog'
import { Avatar, IAvatarUser } from '../lib/avatar'
import { Avatar } from '../lib/avatar'
interface IAccountsProps {
readonly dotComUser: User | null
readonly enterpriseUser: User | null
readonly dotComAccount: Account | null
readonly enterpriseAccount: Account | null
readonly onDotComSignIn: () => void
readonly onEnterpriseSignIn: () => void
readonly onLogout: (user: User) => void
readonly onLogout: (account: Account) => void
}
enum SignInType {
@ -25,31 +26,31 @@ export class Accounts extends React.Component<IAccountsProps, void> {
return (
<DialogContent className='accounts-tab'>
<h2>GitHub.com</h2>
{this.props.dotComUser ? this.renderUser(this.props.dotComUser) : this.renderSignIn(SignInType.DotCom)}
{this.props.dotComAccount ? this.renderAccount(this.props.dotComAccount) : this.renderSignIn(SignInType.DotCom)}
<h2>Enterprise</h2>
{this.props.enterpriseUser ? this.renderUser(this.props.enterpriseUser) : this.renderSignIn(SignInType.Enterprise)}
{this.props.enterpriseAccount ? this.renderAccount(this.props.enterpriseAccount) : this.renderSignIn(SignInType.Enterprise)}
</DialogContent>
)
}
private renderUser(user: User) {
const email = user.emails[0] || ''
private renderAccount(account: Account) {
const email = account.emails.length ? account.emails[0].email : ''
const avatarUser: IAvatarUser = {
name: user.name,
name: account.name,
email: email,
avatarURL: user.avatarURL,
avatarURL: account.avatarURL,
}
return (
<Row className='account-info'>
<Avatar user={avatarUser} />
<div className='user-info'>
<div className='name'>{user.name}</div>
<div className='login'>@{user.login}</div>
<div className='name'>{account.name}</div>
<div className='login'>@{account.login}</div>
</div>
<Button onClick={this.logout(user)}>Log Out</Button>
<Button onClick={this.logout(account)}>Log Out</Button>
</Row>
)
}
@ -93,9 +94,9 @@ export class Accounts extends React.Component<IAccountsProps, void> {
}
}
private logout = (user: User) => {
private logout = (account: Account) => {
return () => {
this.props.onLogout(user)
this.props.onLogout(account)
}
}
}

View File

@ -1,5 +1,5 @@
import * as React from 'react'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { Dispatcher } from '../../lib/dispatcher'
import { TabBar } from '../tab-bar'
import { Accounts } from './accounts'
@ -12,8 +12,8 @@ import { getGlobalConfigValue, setGlobalConfigValue } from '../../lib/git/config
interface IPreferencesProps {
readonly dispatcher: Dispatcher
readonly dotComUser: User | null
readonly enterpriseUser: User | null
readonly dotComAccount: Account | null
readonly enterpriseAccount: Account | null
readonly onDismissed: () => void
}
@ -45,16 +45,16 @@ export class Preferences extends React.Component<IPreferencesProps, IPreferences
let committerEmail = await getGlobalConfigValue('user.email')
if (!committerName || !committerEmail) {
const user = this.props.dotComUser || this.props.enterpriseUser
const account = this.props.dotComAccount || this.props.enterpriseAccount
if (user) {
if (account) {
if (!committerName) {
committerName = user.login
committerName = account.login
}
if (!committerEmail && user.emails.length) {
committerEmail = user.emails[0]
if (!committerEmail && account.emails.length) {
committerEmail = account.emails[0].email
}
}
}
@ -94,8 +94,8 @@ export class Preferences extends React.Component<IPreferencesProps, IPreferences
this.props.dispatcher.showEnterpriseSignInDialog()
}
private onLogout = (user: User) => {
this.props.dispatcher.removeUser(user)
private onLogout = (account: Account) => {
this.props.dispatcher.removeAccount(account)
}
private renderActiveTab() {
@ -103,8 +103,8 @@ export class Preferences extends React.Component<IPreferencesProps, IPreferences
switch (index) {
case PreferencesTab.Accounts:
return <Accounts
dotComUser={this.props.dotComUser}
enterpriseUser={this.props.enterpriseUser}
dotComAccount={this.props.dotComAccount}
enterpriseAccount={this.props.enterpriseAccount}
onDotComSignIn={this.onDotComSignIn}
onEnterpriseSignIn={this.onEnterpriseSignIn}
onLogout={this.onLogout}

View File

@ -1,6 +1,6 @@
import * as React from 'react'
import { User } from '../../models/user'
import { API, IAPIUser } from '../../lib/api'
import { Account } from '../../models/account'
import { API, IAPIUser } from '../../lib/api'
import { TextBox } from '../lib/text-box'
import { Select } from '../lib/select'
import { DialogContent } from '../dialog'
@ -9,7 +9,7 @@ import { merge } from '../../lib/merge'
interface IPublishRepositoryProps {
/** The user to use for publishing. */
readonly user: User
readonly account: Account
/** The settings to use when publishing the repository. */
readonly settings: IPublishRepositorySettings
@ -48,7 +48,7 @@ export class PublishRepository extends React.Component<IPublishRepositoryProps,
}
public async componentWillMount() {
const api = new API(this.props.user)
const api = new API(this.props.account)
const orgs = await api.fetchOrgs()
this.setState({ orgs })
}

View File

@ -1,7 +1,7 @@
import * as React from 'react'
import { PublishRepository, IPublishRepositorySettings } from './publish-repository'
import { Dispatcher } from '../../lib/dispatcher'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { Repository } from '../../models/repository'
import { ButtonGroup } from '../lib/button-group'
import { Button } from '../lib/button'
@ -22,8 +22,8 @@ interface IPublishProps {
/** The repository being published. */
readonly repository: Repository
/** The signed in users. */
readonly users: ReadonlyArray<User>
/** The signed in accounts. */
readonly accounts: ReadonlyArray<Account>
/** The function to call when the dialog should be dismissed. */
readonly onDismissed: () => void
@ -44,10 +44,10 @@ export class Publish extends React.Component<IPublishProps, IPublishState> {
public constructor(props: IPublishProps) {
super(props)
const dotComUser = this.getUserForTab(PublishTab.DotCom)
const enterpriseUser = this.getUserForTab(PublishTab.Enterprise)
const dotComAccount = this.getAccountForTab(PublishTab.DotCom)
const enterpriseAccount = this.getAccountForTab(PublishTab.Enterprise)
let startingTab = PublishTab.DotCom
if (!dotComUser && enterpriseUser) {
if (!dotComAccount && enterpriseAccount) {
startingTab = PublishTab.Enterprise
}
@ -85,10 +85,10 @@ export class Publish extends React.Component<IPublishProps, IPublishState> {
private renderContent() {
const tab = this.state.currentTab
const user = this.getUserForTab(tab)
if (user) {
const account = this.getAccountForTab(tab)
if (account) {
return <PublishRepository
user={user}
account={account}
settings={this.state.publishSettings}
onSettingsChanged={this.onSettingsChanged}/>
} else {
@ -104,13 +104,13 @@ export class Publish extends React.Component<IPublishProps, IPublishState> {
this.setState({ publishSettings: settings })
}
private getUserForTab(tab: PublishTab): User | null {
const users = this.props.users
private getAccountForTab(tab: PublishTab): Account | null {
const accounts = this.props.accounts
switch (tab) {
case PublishTab.DotCom:
return users.find(u => u.endpoint === getDotComAPIEndpoint()) || null
return accounts.find(a => a.endpoint === getDotComAPIEndpoint()) || null
case PublishTab.Enterprise:
return users.find(u => u.endpoint !== getDotComAPIEndpoint()) || null
return accounts.find(a => a.endpoint !== getDotComAPIEndpoint()) || null
default:
return assertNever(tab, `Unknown tab: ${tab}`)
}
@ -140,7 +140,7 @@ export class Publish extends React.Component<IPublishProps, IPublishState> {
private renderFooter() {
const disabled = !this.state.publishSettings.name.length
const tab = this.state.currentTab
const user = this.getUserForTab(tab)
const user = this.getAccountForTab(tab)
if (user) {
return (
<DialogFooter>
@ -169,14 +169,14 @@ export class Publish extends React.Component<IPublishProps, IPublishState> {
private publishRepository = () => {
const tab = this.state.currentTab
const user = this.getUserForTab(tab)
if (!user) {
const account = this.getAccountForTab(tab)
if (!account) {
fatalError(`Tried to publish with no user. That seems impossible!`)
return
}
const settings = this.state.publishSettings
this.props.dispatcher.publishRepository(this.props.repository, settings.name, settings.description, settings.private, user, settings.org)
this.props.dispatcher.publishRepository(this.props.repository, settings.name, settings.description, settings.private, account, settings.org)
this.props.onDismissed()
}

View File

@ -1,11 +1,11 @@
import * as React from 'react'
import { WelcomeStep } from './welcome'
import { User } from '../../models/user'
import { Account } from '../../models/account'
import { ConfigureGitUser } from '../lib/configure-git-user'
import { Button } from '../lib/button'
interface IConfigureGitProps {
readonly users: ReadonlyArray<User>
readonly accounts: ReadonlyArray<Account>
readonly advance: (step: WelcomeStep) => void
}
@ -19,7 +19,7 @@ export class ConfigureGit extends React.Component<IConfigureGitProps, void> {
This is used to identify the commits you create. Anyone will be able to see this information if you publish commits.
</p>
<ConfigureGitUser users={this.props.users} onSave={this.continue} saveLabel='Continue'>
<ConfigureGitUser accounts={this.props.accounts} onSave={this.continue} saveLabel='Continue'>
<Button onClick={this.cancel}>Cancel</Button>
</ConfigureGitUser>
</div>

View File

@ -99,7 +99,7 @@ export class Welcome extends React.Component<IWelcomeProps, IWelcomeState> {
case WelcomeStep.Start: return <Start {...props}/>
case WelcomeStep.SignInToDotCom: return <SignInDotCom {...props} signInState={signInState} />
case WelcomeStep.SignInToEnterprise: return <SignInEnterprise {...props} signInState={signInState} />
case WelcomeStep.ConfigureGit: return <ConfigureGit {...props} users={this.props.appStore.getState().users}/>
case WelcomeStep.ConfigureGit: return <ConfigureGit {...props} accounts={this.props.appStore.getState().accounts}/>
case WelcomeStep.UsageOptOut: return <UsageOptOut {...props} optOut={this.props.appStore.getStatsOptOut()}/>
default: return assertNever(step, `Unknown welcome step: ${step}`)
}

View File

@ -1,12 +1,5 @@
import { Disposable } from 'event-kit'
import { Dispatcher } from '../src/lib/dispatcher'
import { User } from '../src/models/user'
import { Repository } from '../src/models/repository'
type State = {users: ReadonlyArray<User>, repositories: ReadonlyArray<Repository>}
export class InMemoryDispatcher extends Dispatcher {
public onDidUpdate(fn: (state: State) => void): Disposable {
return new Disposable(() => {})
}
}

View File

@ -0,0 +1,24 @@
import * as chai from 'chai'
const expect = chai.expect
import { Account } from '../../src/models/account'
import { Email } from '../../src/models/email'
import { AccountsStore } from '../../src/shared-process/accounts-store'
import { InMemoryStore } from '../in-memory-store'
describe('AccountsStore', () => {
let accountsStore: AccountsStore | null = null
beforeEach(() => {
accountsStore = new AccountsStore(new InMemoryStore(), new InMemoryStore())
})
describe('adding a new user', () => {
it('contains the added user', () => {
const newAccountLogin = 'tonald-drump'
accountsStore!.addAccount(new Account(newAccountLogin, '', '', new Array<Email>(), '', 1, ''))
const users = accountsStore!.getAll()
expect(users[0].login).to.equal(newAccountLogin)
})
})
})

View File

@ -2,40 +2,40 @@ import * as chai from 'chai'
const expect = chai.expect
import { matchGitHubRepository } from '../../src/lib/repository-matching'
import { User } from '../../src/models/user'
import { Account } from '../../src/models/account'
describe('Repository matching', () => {
it('matches HTTPS URLs', () => {
const users = [ new User('alovelace', 'https://api.github.com', '', new Array<string>(), '', 1, '') ]
const repo = matchGitHubRepository(users, 'https://github.com/someuser/somerepo.git')!
const accounts = [ new Account('alovelace', 'https://api.github.com', '', [ ], '', 1, '') ]
const repo = matchGitHubRepository(accounts, 'https://github.com/someuser/somerepo.git')!
expect(repo.name).to.equal('somerepo')
expect(repo.owner.login).to.equal('someuser')
})
it('matches HTTPS URLs without the git extension', () => {
const users = [ new User('alovelace', 'https://api.github.com', '', new Array<string>(), '', 1, '') ]
const repo = matchGitHubRepository(users, 'https://github.com/someuser/somerepo')!
const accounts = [ new Account('alovelace', 'https://api.github.com', '', [ ], '', 1, '') ]
const repo = matchGitHubRepository(accounts, 'https://github.com/someuser/somerepo')!
expect(repo.name).to.equal('somerepo')
expect(repo.owner.login).to.equal('someuser')
})
it('matches git URLs', () => {
const users = [ new User('alovelace', 'https://api.github.com', '', new Array<string>(), '', 1, '') ]
const repo = matchGitHubRepository(users, 'git:github.com/someuser/somerepo.git')!
const accounts = [ new Account('alovelace', 'https://api.github.com', '', [ ], '', 1, '') ]
const repo = matchGitHubRepository(accounts, 'git:github.com/someuser/somerepo.git')!
expect(repo.name).to.equal('somerepo')
expect(repo.owner.login).to.equal('someuser')
})
it('matches SSH URLs', () => {
const users = [ new User('alovelace', 'https://api.github.com', '', new Array<string>(), '', 1, '') ]
const repo = matchGitHubRepository(users, 'git@github.com:someuser/somerepo.git')!
const accounts = [ new Account('alovelace', 'https://api.github.com', '', [ ], '', 1, '') ]
const repo = matchGitHubRepository(accounts, 'git@github.com:someuser/somerepo.git')!
expect(repo.name).to.equal('somerepo')
expect(repo.owner.login).to.equal('someuser')
})
it(`doesn't match if there aren't any users with that endpoint`, () => {
const users = [ new User('alovelace', 'https://github.babbageinc.com', '', new Array<string>(), '', 1, '') ]
const repo = matchGitHubRepository(users, 'https://github.com/someuser/somerepo.git')
const accounts = [ new Account('alovelace', 'https://github.babbageinc.com', '', [ ], '', 1, '') ]
const repo = matchGitHubRepository(accounts, 'https://github.com/someuser/somerepo.git')
expect(repo).to.equal(null)
})
})

View File

@ -1,23 +0,0 @@
import * as chai from 'chai'
const expect = chai.expect
import { User } from '../../src/models/user'
import { UsersStore } from '../../src/shared-process/users-store'
import { InMemoryStore } from '../in-memory-store'
describe('UsersStore', () => {
let usersStore: UsersStore | null = null
beforeEach(() => {
usersStore = new UsersStore(new InMemoryStore(), new InMemoryStore())
})
describe('adding a new user', () => {
it('contains the added user', () => {
const newUserLogin = 'tonald-drump'
usersStore!.addUser(new User(newUserLogin, '', '', new Array<string>(), '', 1, ''))
const users = usersStore!.getUsers()
expect(users[0].login).to.equal(newUserLogin)
})
})
})