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:
commit
ee7f41f91b
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
||||
|
|
|
@ -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.`)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
54
app/src/models/account.ts
Normal 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
11
app/src/models/avatar.ts
Normal 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
43
app/src/models/email.ts
Normal 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
82
app/src/shared-process/accounts-store.ts
Normal file
82
app/src/shared-process/accounts-store.ts
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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 } ])
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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, '..'))
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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(() => {})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
24
app/test/unit/accounts-store-test.ts
Normal file
24
app/test/unit/accounts-store-test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user