Merge branch 'master' into bump-to-npm-5-finally

This commit is contained in:
Brendan Forster 2017-10-01 20:19:23 +11:00
commit 3a8f799545
41 changed files with 737 additions and 271 deletions

View file

@ -3,7 +3,7 @@
"productName": "GitHub Desktop",
"bundleID": "com.github.GitHubClient",
"companyName": "GitHub, Inc.",
"version": "1.0.2-beta0",
"version": "1.0.4-beta0",
"main": "./main.js",
"repository": {
"type": "git",
@ -22,8 +22,8 @@
"classnames": "^2.2.5",
"codemirror": "^5.29.0",
"deep-equal": "^1.0.1",
"dexie": "^1.4.1",
"dugite": "^1.43.0",
"dexie": "^2.0.0",
"dugite": "^1.45.0",
"electron-window-state": "^4.0.2",
"event-kit": "^2.0.0",
"file-uri-to-path": "0.0.2",
@ -32,11 +32,11 @@
"keytar": "^4.0.4",
"moment": "^2.17.1",
"primer-support": "^4.0.0",
"react": "^15.6.1",
"react-addons-shallow-compare": "^15.6.0",
"react-dom": "^15.6.1",
"react": "^15.6.2",
"react-addons-shallow-compare": "^15.6.2",
"react-dom": "^15.6.2",
"react-transition-group": "^1.2.0",
"react-virtualized": "^9.8.0",
"react-virtualized": "^9.10.1",
"runas": "^3.1.1",
"source-map-support": "^0.4.15",
"textarea-caret": "^3.0.2",
@ -53,7 +53,7 @@
"electron-debug": "^1.1.0",
"electron-devtools-installer": "^2.1.0",
"react-addons-perf": "15.4.2",
"react-addons-test-utils": "^15.4.2",
"react-addons-test-utils": "^15.6.2",
"style-loader": "^0.13.2",
"temp": "^0.8.3",
"webpack-hot-middleware": "^2.10.0"

View file

@ -111,8 +111,9 @@ export interface IAPIIssue {
export type APIRefState = 'failure' | 'pending' | 'success'
/** The API response to a ref status request. */
interface IAPIRefStatus {
export interface IAPIRefStatus {
readonly state: APIRefState
readonly total_count: number
}
interface IAPIPullRequestRef {
@ -401,12 +402,12 @@ export class API {
owner: string,
name: string,
ref: string
): Promise<APIRefState> {
): Promise<IAPIRefStatus> {
const path = `repos/${owner}/${name}/commits/${ref}/status`
try {
const response = await this.request('GET', path)
const status = await parsedResponse<IAPIRefStatus>(response)
return status.state
return status
} catch (e) {
log.warn(
`fetchCombinedRefStatus: failed for repository ${owner}/${name} on ref ${ref}`,

View file

@ -1,3 +1,4 @@
export * from './github-user-database'
export * from './issues-database'
export * from './repositories-database'
export * from './pull-request-database'

View file

@ -0,0 +1,21 @@
import Dexie from 'dexie'
export interface IPullRequest {
readonly id?: number
readonly repoId: number
readonly number: number
readonly title: string
readonly lastUpdate?: string
}
export class PullRequestDatabase extends Dexie {
public pullRequests: Dexie.Table<IPullRequest, number>
public constructor(name: string) {
super(name)
this.version(1).stores({
pullRequests: 'id++, repo_id',
})
}
}

View file

@ -24,14 +24,14 @@ export const enum GitResetMode {
Mixed,
}
function resetModeToFlag(mode: GitResetMode): string {
function resetModeToArgs(mode: GitResetMode, ref: string): string[] {
switch (mode) {
case GitResetMode.Hard:
return '--hard'
return ['reset', '--hard', ref]
case GitResetMode.Mixed:
return '--mixed'
return ['reset', ref]
case GitResetMode.Soft:
return '--soft'
return ['reset', '--soft', ref]
default:
return assertNever(mode, `Unknown reset mode: ${mode}`)
}
@ -43,8 +43,8 @@ export async function reset(
mode: GitResetMode,
ref: string
): Promise<true> {
const modeFlag = resetModeToFlag(mode)
await git(['reset', modeFlag, ref, '--'], repository.path, 'reset')
const args = resetModeToArgs(mode, ref)
await git(args, repository.path, 'reset')
return true
}
@ -73,8 +73,25 @@ export async function resetPaths(
return
}
const modeFlag = resetModeToFlag(mode)
await git(['reset', modeFlag, ref, '--', ...paths], repository.path, 'reset')
const baseArgs = resetModeToArgs(mode, ref)
if (__WIN32__ && mode === GitResetMode.Mixed) {
// Git for Windows has experimental support for reading paths to reset
// from standard input. This is helpful in situations where your file
// paths are greater than 32KB in length, because of shell limitations.
//
// This hasn't made it to Git core, so we fallback to the default behaviour
// as macOS and Linux don't have this same shell limitation. See
// https://github.com/desktop/desktop/issues/2833#issuecomment-331352952
// for more context.
const args = [...baseArgs, '--stdin', '-z']
await git(args, repository.path, 'resetPaths', {
stdin: paths.join('\0'),
})
} else {
const args = [...baseArgs, '--', ...paths]
await git(args, repository.path, 'resetPaths')
}
}
/** Unstage all paths. */

View file

@ -1,13 +1,13 @@
import * as Path from 'path'
import { app } from 'electron'
let logFilePath: string | null = null
let logDirectoryPath: string | null = null
export function getLogPath() {
if (!logFilePath) {
export function getLogDirectoryPath() {
if (!logDirectoryPath) {
const userData = app.getPath('userData')
logFilePath = Path.join(userData, 'logs')
logDirectoryPath = Path.join(userData, 'logs')
}
return logFilePath
return logDirectoryPath
}

View file

@ -252,19 +252,16 @@ export class StatsStore {
private async updateDailyMeasures<K extends keyof IDailyMeasures>(
fn: (measures: IDailyMeasures) => Pick<IDailyMeasures, K>
): Promise<void> {
const db = this.db
const defaultMeasures = DefaultDailyMeasures
await this.db.transaction('rw', this.db.dailyMeasures, function*() {
const measures: IDailyMeasures | null = yield db.dailyMeasures
.limit(1)
.first()
await this.db.transaction('rw', this.db.dailyMeasures, async () => {
const measures = await this.db.dailyMeasures.limit(1).first()
const measuresWithDefaults = {
...defaultMeasures,
...measures,
}
const newMeasures = merge(measuresWithDefaults, fn(measuresWithDefaults))
return db.dailyMeasures.put(newMeasures)
return this.db.dailyMeasures.put(newMeasures)
})
}

View file

@ -6,7 +6,6 @@ import { API, getAccountForEndpoint, getDotComAPIEndpoint } from '../api'
import {
GitHubUserDatabase,
IGitHubUser,
IMentionableAssociation,
} from '../databases/github-user-database'
import { fatalError } from '../fatal-error'
@ -156,11 +155,12 @@ export class GitHubUserStore {
// string. So searching with an empty email is gonna give us results, but
// not results that are meaningful.
if (email.length > 0) {
gitUser = await this.database.users
.where('[endpoint+email]')
.equals([account.endpoint, email.toLowerCase()])
.limit(1)
.first()
gitUser =
(await this.database.users
.where('[endpoint+email]')
.equals([account.endpoint, email.toLowerCase()])
.limit(1)
.first()) || null
}
// TODO: Invalidate the stored user in the db after ... some reasonable time
@ -237,27 +237,29 @@ export class GitHubUserStore {
userMap.set(user.email, user)
const db = this.database
let addedUser: IGitHubUser | null = null
await this.database.transaction('rw', this.database.users, function*() {
const existing: ReadonlyArray<IGitHubUser> = yield db.users
.where('[endpoint+login]')
.equals([user.endpoint, user.login])
.toArray()
const match = existing.find(e => e.email === user.email)
if (match) {
if (overwriteEmail) {
user = { ...user, id: match.id }
} else {
user = { ...user, id: match.id, email: match.email }
const addedUser = await this.database.transaction(
'rw',
this.database.users,
async () => {
const existing = await this.database.users
.where('[endpoint+login]')
.equals([user.endpoint, user.login])
.toArray()
const match = existing.find(e => e.email === user.email)
if (match) {
if (overwriteEmail) {
user = { ...user, id: match.id }
} else {
user = { ...user, id: match.id, email: match.email }
}
}
const id = await this.database.users.put(user)
return this.database.users.get(id)
}
)
const id = yield db.users.put(user)
addedUser = yield db.users.get(id)
})
return addedUser
return addedUser || null
}
/**
@ -284,12 +286,11 @@ export class GitHubUserStore {
)
}
const db = this.database
await this.database.transaction(
'rw',
this.database.mentionables,
function*() {
const existing = yield db.mentionables
async () => {
const existing = await this.database.mentionables
.where('[userID+repositoryID]')
.equals([userID, repositoryID])
.limit(1)
@ -298,7 +299,7 @@ export class GitHubUserStore {
return
}
yield db.mentionables.put({ userID, repositoryID })
await this.database.mentionables.put({ userID, repositoryID })
}
)
}
@ -330,21 +331,18 @@ export class GitHubUserStore {
userIDs.add(userID)
}
const db = this.database
await this.database.transaction(
'rw',
this.database.mentionables,
function*() {
const associations: ReadonlyArray<
IMentionableAssociation
> = yield db.mentionables
async () => {
const associations = await this.database.mentionables
.where('repositoryID')
.equals(repositoryID)
.toArray()
for (const association of associations) {
if (!userIDs.has(association.userID)) {
yield db.mentionables.delete(association.id!)
await this.database.mentionables.delete(association.id!)
}
}
}
@ -363,21 +361,18 @@ export class GitHubUserStore {
}
const users = new Array<IGitHubUser>()
const db = this.database
await this.database.transaction(
'r',
this.database.mentionables,
this.database.users,
function*() {
const associations: ReadonlyArray<
IMentionableAssociation
> = yield db.mentionables
async () => {
const associations = await this.database.mentionables
.where('repositoryID')
.equals(repositoryID)
.toArray()
for (const association of associations) {
const user = yield db.users.get(association.userID)
const user = await this.database.users.get(association.userID)
if (user) {
users.push(user)
}

View file

@ -8,3 +8,4 @@ export * from './issues-store'
export * from './repositories-store'
export * from './sign-in-store'
export * from './token-store'
export * from './pull-request-store'

View file

@ -112,26 +112,26 @@ export class IssuesStore {
.first()
}
await this.db.transaction('rw', this.db.issues, function*() {
await this.db.transaction('rw', this.db.issues, async () => {
for (const issue of issuesToDelete) {
const existing = yield findIssueInRepositoryByNumber(
const existing = await findIssueInRepositoryByNumber(
gitHubRepositoryID,
issue.number
)
if (existing) {
yield db.issues.delete(existing.id)
await this.db.issues.delete(existing.id!)
}
}
for (const issue of issuesToUpsert) {
const existing = yield findIssueInRepositoryByNumber(
const existing = await findIssueInRepositoryByNumber(
gitHubRepositoryID,
issue.number
)
if (existing) {
yield db.issues.update(existing.id, issue)
await db.issues.update(existing.id!, issue)
} else {
yield db.issues.add(issue)
await db.issues.add(issue)
}
}
})

View file

@ -0,0 +1,72 @@
import { PullRequestDatabase } from '../databases'
import { GitHubRepository } from '../../models/github-repository'
import { Account } from '../../models/account'
import { API, IAPIPullRequest } from '../api'
import { fatalError } from '../fatal-error'
/** The store for GitHub Pull Requests. */
export class PullRequestStore {
private db: PullRequestDatabase
public constructor(db: PullRequestDatabase) {
this.db = db
}
public async cachePullRequests(
repository: GitHubRepository,
account: Account
) {
const api = API.fromAccount(account)
const prs = await api.fetchPullRequests(
repository.owner.login,
repository.name,
'open'
)
await this.writePullRequests(prs, repository)
}
public async getPullRequests(repository: GitHubRepository) {
const gitHubRepositoryID = repository.dbID
if (!gitHubRepositoryID) {
fatalError(
"Cannot get pull requests for a repository that hasn't been inserted into the database!"
)
return []
}
const pullRequests = await this.db.pullRequests
.where('repo_id')
.equals(gitHubRepositoryID)
.sortBy('number')
return pullRequests
}
private async writePullRequests(
pullRequests: ReadonlyArray<IAPIPullRequest>,
repository: GitHubRepository
): Promise<void> {
const repoId = repository.dbID
if (!repoId) {
fatalError(
"Cannot store pull requests for a repository that hasn't been inserted into the database!"
)
return
}
const table = this.db.pullRequests
const insertablePRs = pullRequests.map(x => {
return { repoId, ...x }
})
await this.db.transaction('rw', table, async () => {
await table.clear()
await table.bulkAdd(insertablePRs)
})
}
}

View file

@ -2,21 +2,12 @@ import { Emitter, Disposable } from 'event-kit'
import {
RepositoriesDatabase,
IDatabaseGitHubRepository,
IDatabaseRepository,
} from '../databases/repositories-database'
import { Owner } from '../../models/owner'
import { GitHubRepository } from '../../models/github-repository'
import { Repository } from '../../models/repository'
import { fatalError } from '../fatal-error'
// NB: We can't use async/await within Dexie transactions. This is because Dexie
// uses its own Promise implementation and TypeScript doesn't know about it. See
// https://github.com/dfahlander/Dexie.js/wiki/Typescript#async-and-await, but
// note that their proposed work around doesn't seem to, you know, work, as of
// TS 1.8.
//
// Instead of using async/await, use generator functions and `yield`.
/** The store for local repositories. */
export class RepositoriesStore {
private db: RepositoriesDatabase
@ -39,23 +30,20 @@ export class RepositoriesStore {
/** Get all the local repositories. */
public async getAll(): Promise<ReadonlyArray<Repository>> {
const inflatedRepos: Repository[] = []
const db = this.db
const transaction = this.db.transaction(
await this.db.transaction(
'r',
this.db.repositories,
this.db.gitHubRepositories,
this.db.owners,
function*() {
const repos: ReadonlyArray<
IDatabaseRepository
> = yield db.repositories.toArray()
async () => {
const repos = await this.db.repositories.toArray()
for (const repo of repos) {
let inflatedRepo: Repository | null = null
if (repo.gitHubRepositoryID) {
const gitHubRepository: IDatabaseGitHubRepository = yield db.gitHubRepositories.get(
const gitHubRepository = (await this.db.gitHubRepositories.get(
repo.gitHubRepositoryID
)
const owner = yield db.owners.get(gitHubRepository.ownerID)
))!
const owner = (await this.db.owners.get(gitHubRepository.ownerID))!
const gitHubRepo = new GitHubRepository(
gitHubRepository.name,
new Owner(owner.login, owner.endpoint),
@ -85,8 +73,6 @@ export class RepositoriesStore {
}
)
await transaction
return inflatedRepos
}
@ -94,16 +80,13 @@ export class RepositoriesStore {
public async addRepository(path: string): Promise<Repository> {
let repository: Repository | null = null
const db = this.db
const transaction = this.db.transaction(
await this.db.transaction(
'r',
this.db.repositories,
this.db.gitHubRepositories,
this.db.owners,
function*() {
const repos: Array<
IDatabaseRepository
> = yield db.repositories.toArray()
async () => {
const repos = await this.db.repositories.toArray()
const existing = repos.find(r => r.path === path)
if (existing === undefined) {
return
@ -116,10 +99,10 @@ export class RepositoriesStore {
return
}
const dbRepo: IDatabaseGitHubRepository = yield db.gitHubRepositories.get(
const dbRepo = (await this.db.gitHubRepositories.get(
existing.gitHubRepositoryID
)
const dbOwner = yield db.owners.get(dbRepo.ownerID)
))!
const dbOwner = (await this.db.owners.get(dbRepo.ownerID))!
const owner = new Owner(dbOwner.login, dbOwner.endpoint)
const gitHubRepo = new GitHubRepository(
@ -136,8 +119,6 @@ export class RepositoriesStore {
}
)
await transaction
if (repository !== null) {
return repository
}
@ -239,40 +220,40 @@ export class RepositoriesStore {
}
let gitHubRepositoryID: number | null = null
const db = this.db
const transaction = this.db.transaction(
await this.db.transaction(
'rw',
this.db.repositories,
this.db.gitHubRepositories,
this.db.owners,
function*() {
const localRepo = yield db.repositories.get(repoID)
async () => {
const localRepo = (await this.db.repositories.get(repoID))!
let existingGitHubRepo: IDatabaseGitHubRepository | null = null
let ownerID: number | null = null
if (localRepo.gitHubRepositoryID) {
gitHubRepositoryID = localRepo.gitHubRepositoryID
existingGitHubRepo = yield db.gitHubRepositories.get(
localRepo.gitHubRepositoryID
)
existingGitHubRepo =
(await this.db.gitHubRepositories.get(
localRepo.gitHubRepositoryID
)) || null
if (!existingGitHubRepo) {
return fatalError(`Couldn't look up an existing GitHub repository.`)
}
const owner = yield db.owners.get(existingGitHubRepo.ownerID)
ownerID = owner.id
const owner = (await this.db.owners.get(existingGitHubRepo.ownerID))!
ownerID = owner.id || null
} else {
const owner = newGitHubRepo.owner
const existingOwner = yield db.owners
const existingOwner = await this.db.owners
.where('[endpoint+login]')
.equals([owner.endpoint, owner.login.toLowerCase()])
.limit(1)
.first()
if (existingOwner) {
ownerID = existingOwner.id
ownerID = existingOwner.id || null
} else {
ownerID = yield db.owners.add({
ownerID = await this.db.owners.add({
login: owner.login.toLowerCase(),
endpoint: owner.endpoint,
})
@ -293,13 +274,11 @@ export class RepositoriesStore {
updatedInfo = { ...updatedInfo, id: existingGitHubRepo.id }
}
gitHubRepositoryID = yield db.gitHubRepositories.put(updatedInfo)
yield db.repositories.update(localRepo.id, { gitHubRepositoryID })
gitHubRepositoryID = await this.db.gitHubRepositories.put(updatedInfo)
await this.db.repositories.update(localRepo.id!, { gitHubRepositoryID })
}
)
await transaction
this.emitUpdate()
return repository.withGitHubRepository(

View file

@ -1,7 +1,7 @@
import * as Path from 'path'
import * as winston from 'winston'
import { getLogPath } from '../lib/logging/get-log-path'
import { getLogDirectoryPath } from '../lib/logging/get-log-path'
import { LogLevel } from '../lib/logging/log-level'
import { mkdirIfNeeded } from '../lib/file-system'
@ -71,11 +71,16 @@ function getLogger(): Promise<winston.LogMethod> {
}
loggerPromise = new Promise<winston.LogMethod>((resolve, reject) => {
const logPath = getLogPath()
const logDirectory = getLogDirectoryPath()
mkdirIfNeeded(logPath)
mkdirIfNeeded(logDirectory)
.then(() => {
resolve(initializeWinston(getLogFilePath(logPath)))
try {
const logger = initializeWinston(getLogFilePath(logDirectory))
resolve(logger)
} catch (err) {
reject(err)
}
})
.catch(error => {
reject(error)

View file

@ -313,7 +313,7 @@ app.on('ready', () => {
return
}
if (stats.isDirectory) {
if (stats.isDirectory()) {
const fileURL = Url.format({
pathname: path,
protocol: 'file:',

View file

@ -1,7 +1,7 @@
import { Menu, ipcMain, shell, app } from 'electron'
import { ensureItemIds } from './ensure-item-ids'
import { MenuEvent } from './menu-event'
import { getLogPath } from '../../lib/logging/get-log-path'
import { getLogDirectoryPath } from '../../lib/logging/get-log-path'
import { mkdirIfNeeded } from '../../lib/file-system'
import { log } from '../log'
@ -340,7 +340,7 @@ export function buildDefaultMenu(
const showLogsItem: Electron.MenuItemConstructorOptions = {
label: __DARWIN__ ? 'Show Logs in Finder' : 'S&how logs in Explorer',
click() {
const logPath = getLogPath()
const logPath = getLogDirectoryPath()
mkdirIfNeeded(logPath)
.then(() => {
shell.showItemInFolder(logPath)

View file

@ -1,7 +1,7 @@
import { IAPIPullRequest, APIRefState } from '../lib/api'
import { IAPIPullRequest, IAPIRefStatus } from '../lib/api'
/** A pull request as used in the UI. */
export interface IPullRequest extends IAPIPullRequest {
readonly state: APIRefState
readonly status: IAPIRefStatus
readonly created: Date
}

View file

@ -45,7 +45,7 @@ interface ICreateRepositoryProps {
readonly onDismissed: () => void
/** Prefills path input so user doesn't have to. */
readonly path?: string
readonly initialPath?: string
}
interface ICreateRepositoryState {
@ -86,9 +86,17 @@ export class CreateRepository extends React.Component<
public constructor(props: ICreateRepositoryProps) {
super(props)
const path = this.props.initialPath
? this.props.initialPath
: getDefaultDir()
const name = this.props.initialPath
? sanitizedRepositoryName(Path.basename(this.props.initialPath))
: ''
this.state = {
path: this.props.path ? this.props.path : getDefaultDir(),
name: '',
path,
name,
description: '',
createWithReadme: false,
creating: false,
@ -103,25 +111,23 @@ export class CreateRepository extends React.Component<
public async componentDidMount() {
const gitIgnoreNames = await getGitIgnoreNames()
this.setState({ ...this.state, gitIgnoreNames })
this.setState({ gitIgnoreNames })
const licenses = await getLicenses()
this.setState({ ...this.state, licenses })
this.setState({ licenses })
const isRepository = await isGitRepository(this.state.path)
this.setState({ isRepository })
}
private onPathChanged = async (event: React.FormEvent<HTMLInputElement>) => {
const path = event.currentTarget.value
private onPathChanged = async (path: string) => {
const isRepository = await isGitRepository(path)
this.setState({ isRepository, path, isValidPath: null })
}
private onNameChanged = (event: React.FormEvent<HTMLInputElement>) => {
const name = event.currentTarget.value
this.setState({ ...this.state, name })
private onNameChanged = (name: string) => {
this.setState({ name })
}
private onDescriptionChanged = (description: string) => {
@ -155,17 +161,29 @@ export class CreateRepository extends React.Component<
})
}
private resolveRepositoryRoot = async (): Promise<string> => {
const currentPath = this.state.path
if (this.props.initialPath && this.props.initialPath === currentPath) {
// if the user provided an initial path and didn't change it, we should
// validate it is an existing path and use that for the repository
try {
await this.ensureDirectory(currentPath)
return currentPath
} catch {}
}
return Path.join(currentPath, sanitizedRepositoryName(this.state.name))
}
private createRepository = async () => {
const fullPath = Path.join(
this.state.path,
sanitizedRepositoryName(this.state.name)
)
const fullPath = await this.resolveRepositoryRoot()
try {
await this.ensureDirectory(fullPath)
this.setState({ ...this.state, isValidPath: true })
this.setState({ isValidPath: true })
} catch (e) {
if (e.code === 'EACCES' && e.errno === -13) {
return this.setState({ ...this.state, isValidPath: false })
return this.setState({ isValidPath: false })
}
log.error(
@ -175,12 +193,12 @@ export class CreateRepository extends React.Component<
return this.props.dispatcher.postError(e)
}
this.setState({ ...this.state, creating: true })
this.setState({ creating: true })
try {
await initGitRepository(fullPath)
} catch (e) {
this.setState({ ...this.state, creating: false })
this.setState({ creating: false })
log.error(
`createRepository: unable to initialize a Git repository at ${fullPath}`,
e
@ -275,19 +293,27 @@ export class CreateRepository extends React.Component<
this.props.dispatcher.postError(e)
}
this.setState({ ...this.state, creating: false })
this.setState({ creating: false })
setDefaultDir(this.state.path)
this.updateDefaultDirectory()
this.props.dispatcher.selectRepository(repository)
this.props.onDismissed()
}
private updateDefaultDirectory = () => {
// don't update the default directory as a result of creating the
// repository from an empty folder, because this value will be the
// repository path itself
if (!this.props.initialPath) {
setDefaultDir(this.state.path)
}
}
private onCreateWithReadmeChange = (
event: React.FormEvent<HTMLInputElement>
) => {
this.setState({
...this.state,
createWithReadme: event.currentTarget.checked,
})
}
@ -308,12 +334,12 @@ export class CreateRepository extends React.Component<
private onGitIgnoreChange = (event: React.FormEvent<HTMLSelectElement>) => {
const gitIgnore = event.currentTarget.value
this.setState({ ...this.state, gitIgnore })
this.setState({ gitIgnore })
}
private onLicenseChange = (event: React.FormEvent<HTMLSelectElement>) => {
const license = event.currentTarget.value
this.setState({ ...this.state, license })
this.setState({ license })
}
private renderGitIgnores() {
@ -419,6 +445,8 @@ export class CreateRepository extends React.Component<
this.state.creating ||
this.state.isRepository
const readOnlyPath = !!this.props.initialPath
return (
<Dialog
id="create-repository"
@ -435,7 +463,7 @@ export class CreateRepository extends React.Component<
value={this.state.name}
label="Name"
placeholder="repository name"
onChange={this.onNameChanged}
onValueChanged={this.onNameChanged}
autoFocus={true}
/>
</Row>
@ -455,9 +483,12 @@ export class CreateRepository extends React.Component<
value={this.state.path}
label={__DARWIN__ ? 'Local Path' : 'Local path'}
placeholder="repository path"
onChange={this.onPathChanged}
onValueChanged={this.onPathChanged}
disabled={readOnlyPath}
/>
<Button onClick={this.showFilePicker}>Choose</Button>
<Button onClick={this.showFilePicker} disabled={readOnlyPath}>
Choose
</Button>
</Row>
{this.renderInvalidPathWarning()}

View file

@ -977,7 +977,7 @@ export class App extends React.Component<IAppProps, IAppState> {
key="create-repository"
onDismissed={this.onPopupDismissed}
dispatcher={this.props.dispatcher}
path={popup.path}
initialPath={popup.path}
/>
)
case PopupType.CloneRepository:

View file

@ -251,10 +251,7 @@ export abstract class AutocompletingTextInput<
protected abstract getElementTagName(): 'textarea' | 'input'
private renderTextInput() {
return React.createElement<
React.HTMLAttributes<ElementType>,
ElementType
>(this.getElementTagName(), {
const props = {
type: 'text',
placeholder: this.props.placeholder,
value: this.props.value,
@ -262,7 +259,12 @@ export abstract class AutocompletingTextInput<
onChange: this.onChange,
onKeyDown: this.onKeyDown,
onBlur: this.onBlur,
})
}
return React.createElement<React.HTMLAttributes<ElementType>, ElementType>(
this.getElementTagName(),
props
)
}
private onBlur = (e: React.FocusEvent<ElementType>) => {

View file

@ -0,0 +1,46 @@
import * as React from 'react'
import { Octicon, OcticonSymbol } from '../octicons'
import { IAPIRefStatus, APIRefState } from '../../lib/api'
import { assertNever } from '../../lib/fatal-error'
import * as classNames from 'classnames'
interface ICIStatusProps {
/** The classname for the underlying element. */
readonly className?: string
/** The status to display. */
readonly status: IAPIRefStatus
}
/** The little CI status indicator. */
export class CIStatus extends React.Component<ICIStatusProps, {}> {
public render() {
const status = this.props.status
const state = status.state
const ciTitle = `Commit status: ${state}`
return (
<Octicon
className={classNames(
'ci-status',
`ci-status-${state}`,
this.props.className
)}
symbol={getSymbolForState(state)}
title={ciTitle}
/>
)
}
}
function getSymbolForState(state: APIRefState): OcticonSymbol {
switch (state) {
case 'pending':
return OcticonSymbol.primitiveDot
case 'failure':
return OcticonSymbol.x
case 'success':
return OcticonSymbol.check
}
return assertNever(state, `Unknown state: ${state}`)
}

View file

@ -2,7 +2,7 @@ import * as React from 'react'
import { Ref } from '../lib/ref'
import { LinkButton } from '../lib/link-button'
const BlankSlateImage = `file:///${__dirname}/static/empty-no-file-selected.svg`
const BlankSlateImage = `file:///${__dirname}/static/empty-no-pull-requests.svg`
interface INoPullRequestsProps {
/** The name of the repository. */

View file

@ -0,0 +1,30 @@
import * as React from 'react'
import { CIStatus } from './ci-status'
import { IAPIRefStatus } from '../../lib/api'
interface IPullRequestBadgeProps {
/** The CI status of the pull request. */
readonly status: IAPIRefStatus
/** The pull request's number. */
readonly number: number
}
/** The pull request info badge. */
export class PullRequestBadge extends React.Component<
IPullRequestBadgeProps,
{}
> {
public render() {
const status = this.props.status
return (
<div className="pr-badge">
<span className="number">#{this.props.number}</span>
{status.total_count > 0 ? (
<CIStatus status={this.props.status} />
) : null}
</div>
)
}
}

View file

@ -1,8 +1,8 @@
import * as React from 'react'
import * as moment from 'moment'
import { Octicon, OcticonSymbol } from '../octicons'
import { APIRefState } from '../../lib/api'
import { assertNever } from '../../lib/fatal-error'
import { IAPIRefStatus } from '../../lib/api'
import { CIStatus } from './ci-status'
interface IPullRequestListItemProps {
/** The title. */
@ -18,7 +18,7 @@ interface IPullRequestListItemProps {
readonly author: string
/** The CI status. */
readonly status: APIRefState
readonly status: IAPIRefStatus
}
/** Pull requests as rendered in the Pull Requests list. */
@ -30,7 +30,6 @@ export class PullRequestListItem extends React.Component<
const timeAgo = moment(this.props.created).fromNow()
const { title, author, status } = this.props
const subtitle = `#${this.props.number} opened ${timeAgo} by ${author}`
const ciTitle = `Commit status: ${status}`
return (
<div className="pull-request-item">
<Octicon className="icon" symbol={OcticonSymbol.gitPullRequest} />
@ -44,25 +43,8 @@ export class PullRequestListItem extends React.Component<
</div>
</div>
<Octicon
className={`status status-${status}`}
symbol={getSymbolForStatus(status)}
title={ciTitle}
/>
{status.total_count > 0 ? <CIStatus status={status} /> : null}
</div>
)
}
}
function getSymbolForStatus(status: APIRefState): OcticonSymbol {
switch (status) {
case 'pending':
return OcticonSymbol.primitiveDot
case 'failure':
return OcticonSymbol.x
case 'success':
return OcticonSymbol.check
}
return assertNever(status, `Unknown status: ${status}`)
}

View file

@ -21,7 +21,7 @@ const PullRequestFilterList: new () => FilterList<
IPullRequestListItem
> = FilterList as any
const RowHeight = 45
const RowHeight = 47
interface IPullRequestListProps {
/** The pull requests to display. */
@ -89,7 +89,7 @@ export class PullRequestList extends React.Component<
number={pr.number}
created={pr.created}
author={pr.user.login}
status={pr.state}
status={pr.status}
/>
)
}

View file

@ -367,7 +367,7 @@ export class Dialog extends React.Component<IDialogProps, IDialogState> {
>
{this.renderHeader()}
<form onSubmit={this.onSubmit} autoFocus={true}>
<form onSubmit={this.onSubmit}>
<fieldset disabled={this.props.disabled}>
{this.props.children}
</fieldset>

View file

@ -39,7 +39,6 @@ export class LinkButton extends React.Component<ILinkButtonProps, {}> {
onClick={this.onClick}
title={this.props.title}
tabIndex={this.props.tabIndex}
disabled={this.props.disabled}
>
{this.props.children}
</a>

View file

@ -564,9 +564,9 @@ export class List extends React.Component<IListProps, IListState> {
return (
<Grid
aria-label={null!}
aria-label={''}
key="grid"
role={null!}
role={''}
ref={this.onGridRef}
autoContainerWidth={true}
width={width}

View file

@ -13,6 +13,8 @@ import { enablePreviewFeatures } from '../../lib/feature-flag'
import { API } from '../../lib/api'
import { IPullRequest } from '../../models/pull-request'
import { GitHubRepository } from '../../models/github-repository'
import { Branch } from '../../models/branch'
import { PullRequestBadge } from '../branches/pull-request-badge'
const RefreshPullRequestInterval = 1000 * 60 * 10
@ -128,8 +130,9 @@ export class BranchDropdown extends React.Component<
const pullRequestsWithStatus: Array<IPullRequest> = []
for (const pr of pullRequests) {
const created = new Date(pr.created_at)
try {
const state = await api.fetchCombinedRefStatus(
const status = await api.fetchCombinedRefStatus(
repository.owner.login,
repository.name,
pr.head.sha
@ -137,14 +140,14 @@ export class BranchDropdown extends React.Component<
pullRequestsWithStatus.push({
...pr,
state,
created: new Date(pr.created_at),
status,
created,
})
} catch (e) {
pullRequestsWithStatus.push({
...pr,
state: 'pending',
created: new Date(pr.created_at),
status: { state: 'pending', total_count: 0 },
created,
})
}
}
@ -170,6 +173,20 @@ export class BranchDropdown extends React.Component<
this.setState({ pullRequests })
}
private get currentPullRequest(): IPullRequest | null {
const repositoryState = this.props.repositoryState
const branchesState = repositoryState.branchesState
const pullRequests = this.state.pullRequests
const gitHubRepository = this.props.repository.gitHubRepository
const tip = branchesState.tip
if (tip.kind === TipState.Valid && pullRequests && gitHubRepository) {
return findCurrentPullRequest(tip.branch, pullRequests, gitHubRepository)
} else {
return null
}
}
private onDropDownStateChanged = (state: DropdownState) => {
// Don't allow opening the drop down when checkout is in progress
if (state === 'open' && this.props.repositoryState.checkoutProgress) {
@ -193,6 +210,10 @@ export class BranchDropdown extends React.Component<
let canOpen = true
let tooltip: string
if (this.currentPullRequest) {
icon = OcticonSymbol.gitPullRequest
}
if (tip.kind === TipState.Unknown) {
// TODO: this is bad and I feel bad
return null
@ -246,7 +267,45 @@ export class BranchDropdown extends React.Component<
dropdownState={currentState}
showDisclosureArrow={canOpen}
progressValue={progressValue}
/>
>
{this.renderPullRequestInfo()}
</ToolbarDropdown>
)
}
private renderPullRequestInfo() {
const pr = this.currentPullRequest
if (!pr) {
return null
}
if (!enablePreviewFeatures()) {
return null
}
return <PullRequestBadge number={pr.number} status={pr.status} />
}
}
function findCurrentPullRequest(
currentBranch: Branch,
pullRequests: ReadonlyArray<IPullRequest>,
gitHubRepository: GitHubRepository
): IPullRequest | null {
const upstream = currentBranch.upstreamWithoutRemote
if (!upstream) {
return null
}
for (const pr of pullRequests) {
if (
pr.head.ref === upstream &&
// TODO: This doesn't work for when I've checked out a PR from a fork.
pr.head.repo.clone_url === gitHubRepository.cloneURL
) {
return pr
}
}
return null
}

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="257px" height="85px" viewBox="0 0 257 85" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<g id="no-pull-requests" fill="#C9DEF2">
<g id="stars-right" transform="translate(168.000000, 1.000000)" fill-rule="nonzero">
<path d="M44.4563773,27.4782814 C44.6852313,28.222572 43.9889658,28.9188375 43.2444173,28.6913354 L41.3825382,28.118141 L39.520271,28.6917568 C38.776199,28.9184823 38.0797631,28.2227985 38.3086867,27.4782814 L38.8818589,25.6174689 L38.3087287,23.7557907 C38.0793136,23.0117418 38.7771315,22.3146776 39.5217186,22.5436227 L41.3825311,23.1167948 L43.2442093,22.5436646 C43.9880322,22.3143193 44.6856807,23.0119678 44.4563994,23.7555827 L43.8832013,25.6174495 L44.4563773,27.4782814 Z M42.9251036,25.9040013 C42.8669289,25.7170111 42.8669289,25.5158387 42.9246646,25.3323533 L43.4837707,23.5162293 L41.6690653,24.0748964 C41.4823298,24.1329918 41.2827342,24.1329918 41.0973455,24.0753133 L39.2811914,23.5158989 L39.8394851,25.3294153 C39.8981351,25.5158387 39.8981351,25.7170111 39.8403773,25.9026545 L39.2808613,27.7191387 L41.0959987,27.1600396 C41.2827342,27.1019441 41.4823298,27.1019441 41.6676467,27.1596006 L43.4841014,27.7188095 L42.9251036,25.9040013 Z"></path>
<path d="M2.928,1.36489322 L1.20955339,-0.353553391 C1.01429124,-0.548815536 0.697708755,-0.548815536 0.502446609,-0.353553391 C0.307184464,-0.158291245 0.307184464,0.158291245 0.502446609,0.353553391 L2.22089322,2.072 L0.502446609,3.79044661 C0.307184464,3.98570876 0.307184464,4.30229124 0.502446609,4.49755339 C0.697708755,4.69281554 1.01429124,4.69281554 1.20955339,4.49755339 L2.928,2.77910678 L4.64644661,4.49755339 C4.84170876,4.69281554 5.15829124,4.69281554 5.35355339,4.49755339 C5.54881554,4.30229124 5.54881554,3.98570876 5.35355339,3.79044661 L3.63510678,2.072 L5.35355339,0.353553391 C5.54881554,0.158291245 5.54881554,-0.158291245 5.35355339,-0.353553391 C5.15829124,-0.548815536 4.84170876,-0.548815536 4.64644661,-0.353553391 L2.928,1.36489322 Z"></path>
<path d="M66.5,13.267 C66.5,12.8436424 66.1563576,12.5 65.732,12.5 C65.3085486,12.5 64.965,12.843736 64.965,13.267 C64.965,13.6911704 65.3084551,14.035 65.732,14.035 C66.1564513,14.035 66.5,13.6912641 66.5,13.267 Z M67.5,13.267 C67.5,14.2434269 66.7088581,15.035 65.732,15.035 C64.7559262,15.035 63.965,14.2432113 63.965,13.267 C63.965,12.2915732 64.7561418,11.5 65.732,11.5 C66.7086424,11.5 67.5,12.2913576 67.5,13.267 Z"></path>
<path d="M32.5,65.5 L32.5,65 C32.5,64.7238576 32.2761424,64.5 32,64.5 C31.7238576,64.5 31.5,64.7238576 31.5,65 L31.5,65.5 L31,65.5 C30.7238576,65.5 30.5,65.7238576 30.5,66 C30.5,66.2761424 30.7238576,66.5 31,66.5 L31.5,66.5 L31.5,67 C31.5,67.2761424 31.7238576,67.5 32,67.5 C32.2761424,67.5 32.5,67.2761424 32.5,67 L32.5,66.5 L33,66.5 C33.2761424,66.5 33.5,66.2761424 33.5,66 C33.5,65.7238576 33.2761424,65.5 33,65.5 L32.5,65.5 Z"></path>
<path d="M87.5,81.508 C87.5,80.9517457 87.0484611,80.5 86.492,80.5 C85.9361424,80.5 85.484,80.9521424 85.484,81.508 C85.484,82.0644611 85.9357457,82.516 86.492,82.516 C87.0488576,82.516 87.5,82.0648576 87.5,81.508 Z M88.5,81.508 C88.5,82.6171424 87.6011424,83.516 86.492,83.516 C85.3835635,83.516 84.484,82.6168484 84.484,81.508 C84.484,80.3998576 85.3838576,79.5 86.492,79.5 C87.6008484,79.5 88.5,80.3995635 88.5,81.508 Z"></path>
<path d="M84.3080852,41.8906809 C84.2241857,41.8447961 84.1552039,41.7758143 84.1093191,41.6919148 L83.207,40.0420367 L82.3046809,41.6919148 C82.2587961,41.7758143 82.1898143,41.8447961 82.1059148,41.8906809 L80.4560367,42.793 L82.1059148,43.6953191 C82.1898143,43.7412039 82.2587961,43.8101857 82.3046809,43.8940852 L83.207,45.5439633 L84.1093191,43.8940852 C84.1552039,43.8101857 84.2241857,43.7412039 84.3080852,43.6953191 L85.9579633,42.793 L84.3080852,41.8906809 Z M87.2399148,42.3543191 C87.5866951,42.5439735 87.5866951,43.0420265 87.2399148,43.2316809 L84.9164079,44.5024079 L83.6456809,46.8259148 C83.4560265,47.1726951 82.9579735,47.1726951 82.7683191,46.8259148 L81.4975921,44.5024079 L79.1740852,43.2316809 C78.8273049,43.0420265 78.8273049,42.5439735 79.1740852,42.3543191 L81.4975921,41.0835921 L82.7683191,38.7600852 C82.9579735,38.4133049 83.4560265,38.4133049 83.6456809,38.7600852 L84.9164079,41.0835921 L87.2399148,42.3543191 Z"></path>
</g>
<g id="ufo" transform="translate(98.000000, 14.000000)">
<g id="bubble" transform="translate(25.000000, 0.000000)">
<path d="M18.3237816,1.08100676 C26.4127649,2.49929537 31.8207464,10.2055364 30.4024578,18.2945197 C29.0755113,25.8645039 22.2411679,31.0843635 14.741292,30.561728 C10.9147571,33.9846722 6.23326239,34.8447564 2.90985691,34.9832289 C2.24602595,35.0115481 1.65540212,34.4838756 2.69592758,33.9120996 C4.73861105,32.7871827 6.04047637,30.4481746 6.80684544,27.6003518 C2.49561727,24.3446731 0.11187458,18.8476715 1.11026872,13.1596829 C2.52855732,5.07069967 10.2347984,-0.337281838 18.3237816,1.08100676 Z M24.0793651,12.0396825 L22.031746,9.99206349 L13.1587302,18.8650794 L9.74603175,15.452381 L7.6984127,17.5 L13.1587302,22.9603175 L24.0793651,12.0396825 Z"></path>
</g>
<g transform="translate(0.000000, 28.000000)" fill-rule="nonzero">
<path d="M19.1258644,7.18619476 C17.3387658,2.59580412 12.1701462,0.322197192 7.58107879,2.10878069 C2.99081125,3.89451596 0.71776611,9.06348268 2.50377128,13.653497 L5.28146848,20.7925262 L21.9035256,14.3251313 L19.1258644,7.18619476 Z M22.7321025,15.0757714 L5.17810255,21.9057714 C4.92075379,22.005902 4.63095952,21.8784516 4.53082872,21.621103 L1.57183195,14.0161113 C-0.414441594,8.91140932 2.11349668,3.16280864 7.21840524,1.17686444 C12.322129,-0.810081455 18.0703182,1.71847042 20.0577713,6.82349704 L23.0167713,14.428497 C23.1169022,14.685846 22.9894516,14.9756407 22.7321025,15.0757714 Z"></path>
<path d="M5.58319378,13.9212093 C3.60174217,9.73688853 5.4726276,5.44590376 9.79150569,4.30974805 C10.0585619,4.23949428 10.2181018,3.96605052 10.147848,3.69899431 C10.0775943,3.4319381 9.80415052,3.27239819 9.53709431,3.34265195 C4.60116378,4.641134 2.43495008,9.60947137 4.67940622,14.3491907 C4.79759001,14.5987647 5.09571664,14.7052776 5.34529068,14.5870938 C5.59486472,14.46891 5.70137757,14.1707834 5.58319378,13.9212093 Z"></path>
<path d="M25.780447,17.5643276 L24.5390132,14.3732856 L3.37095301,22.6096724 L4.61238676,25.8007144 L25.780447,17.5643276 Z M26.6090066,18.3149699 L4.50900656,26.9139699 C4.25164935,27.0141063 3.96184446,26.8866454 3.861721,26.6292831 L2.257721,22.5062831 C2.15760466,22.2489392 2.28505453,21.9591594 2.54239344,21.8590301 L24.6423934,13.2600301 C24.8997507,13.1598937 25.1895555,13.2873546 25.289679,13.5447169 L26.893679,17.6677169 C26.9937953,17.9250608 26.8663455,18.2148406 26.6090066,18.3149699 Z"></path>
<path d="M23.6038134,16.0373224 L13.6898134,19.8943224 C13.432461,19.9944442 13.3050005,20.2842343 13.4051224,20.5415866 C13.5052442,20.798939 13.7950343,20.9263995 14.0523866,20.8262776 L23.9663866,16.9692776 C24.223739,16.8691558 24.3511995,16.5793657 24.2510776,16.3220134 C24.1509558,16.064661 23.8611657,15.9372005 23.6038134,16.0373224 Z"></path>
<path d="M21.8341425,20.1734462 L9.28475399,25.055539 L10.3080248,26.4913986 L22.0498283,21.9216359 L21.8341425,20.1734462 Z M22.775044,22.7124553 L10.308044,27.5644553 C10.0947599,27.6474628 9.85234428,27.5750598 9.71951922,27.3886789 L8.09751922,25.1126789 C7.90835678,24.8472451 8.01965558,24.4746934 8.32341962,24.3565199 L22.0704196,19.0085199 C22.3742037,18.8903387 22.7080239,19.0897663 22.7479375,19.4132759 L23.0899375,22.1852759 C23.1179533,22.4123515 22.9882626,22.6294733 22.775044,22.7124553 Z"></path>
<path d="M13.9890595,29.7871044 C13.8939636,29.3084338 13.4274357,28.9969537 12.9485581,29.0920903 C12.4689981,29.1880023 12.1576943,29.6540686 12.2528154,30.1328675 C12.3486888,30.6128735 12.8143264,30.9236935 13.2934419,30.8285097 C13.7733321,30.7325316 14.0843036,30.2671602 13.9890595,29.7871044 Z M14.9699157,29.5923708 C15.1725741,30.6138234 14.5102845,31.6049451 13.4889292,31.8092157 C12.4679598,32.0120483 11.4761687,31.350013 11.2720843,30.3282292 C11.0693223,29.3076151 11.7319182,28.3156144 12.7530708,28.1113843 C13.774013,27.9085571 14.7671254,28.5716144 14.9699157,29.5923708 Z"></path>
<path d="M13.3409694,28.5043382 L12.8669694,26.1243382 C12.8130323,25.8535146 12.5497617,25.6776934 12.2789382,25.7316306 C12.0081146,25.7855677 11.8322934,26.0488383 11.8862306,26.3196618 L12.3602306,28.6996618 C12.4141677,28.9704854 12.6774383,29.1463066 12.9482618,29.0923694 C13.2190854,29.0384323 13.3949066,28.7751617 13.3409694,28.5043382 Z"></path>
<path d="M23.0780618,25.9222451 C22.8250137,25.5043339 22.2798654,25.3709655 21.863232,25.6236245 C21.4444833,25.8784593 21.3111718,26.4223703 21.5647145,26.840045 C21.8183484,27.2578703 22.3627792,27.3908655 22.780845,27.1370855 C23.1985936,26.8834981 23.3315905,26.3392582 23.0780618,25.9222451 Z M23.9330043,25.4035231 C24.4731556,26.291982 24.189766,27.4516478 23.299755,27.9919145 C22.409669,28.5322266 21.2501595,28.248978 20.7098855,27.358955 C20.1697285,26.4691244 20.4536212,25.3108422 21.3440331,24.7689715 C22.2335372,24.2295478 23.3940719,24.5134681 23.9330043,25.4035231 Z"></path>
<path d="M22.0304133,24.9372432 L20.7714133,22.8632432 C20.6281193,22.6271893 20.3205971,22.5519926 20.0845432,22.6952867 C19.8484893,22.8385807 19.7732926,23.1461029 19.9165867,23.3821568 L21.1755867,25.4561568 C21.3188807,25.6922107 21.6264029,25.7674074 21.8624568,25.6241133 C22.0985107,25.4808193 22.1737074,25.1732971 22.0304133,24.9372432 Z"></path>
</g>
</g>
<g id="stars-left" fill-rule="nonzero">
<path d="M86.072,2.36489322 L84.3535534,0.646446609 C84.1582912,0.451184464 83.8417088,0.451184464 83.6464466,0.646446609 C83.4511845,0.841708755 83.4511845,1.15829124 83.6464466,1.35355339 L85.3648932,3.072 L83.6464466,4.79044661 C83.4511845,4.98570876 83.4511845,5.30229124 83.6464466,5.49755339 C83.8417088,5.69281554 84.1582912,5.69281554 84.3535534,5.49755339 L86.072,3.77910678 L87.7904466,5.49755339 C87.9857088,5.69281554 88.3022912,5.69281554 88.4975534,5.49755339 C88.6928155,5.30229124 88.6928155,4.98570876 88.4975534,4.79044661 L86.7791068,3.072 L88.4975534,1.35355339 C88.6928155,1.15829124 88.6928155,0.841708755 88.4975534,0.646446609 C88.3022912,0.451184464 87.9857088,0.451184464 87.7904466,0.646446609 L86.072,2.36489322 Z"></path>
<path d="M52.5497555,35.5 L51,35.5 C50.7238576,35.5 50.5,35.2761424 50.5,35 C50.5,34.7238576 50.7238576,34.5 51,34.5 L52.550561,34.5 C52.6451922,34.0354667 52.8726579,33.5927746 53.2329466,33.2329466 C54.2102088,32.2556845 55.7947912,32.2556845 56.7718271,33.2327205 C57.132214,33.5926465 57.3597362,34.0354127 57.4543937,34.5 L59.25,34.5 C59.5261424,34.5 59.75,34.7238576 59.75,35 C59.75,35.2761424 59.5261424,35.5 59.25,35.5 L57.4552308,35.5 C57.361082,35.9662119 57.1332807,36.4108259 56.7718271,36.7722795 C55.7946168,37.7482402 54.2103832,37.7482402 53.2331729,36.7722795 C52.8717163,36.410823 52.6439143,35.9662046 52.5497555,35.5 Z M53.9400534,36.0649466 C54.5266168,36.6507598 55.4783832,36.6507598 56.0649466,36.0649466 C56.6516468,35.4782465 56.6516468,34.5260034 56.0649466,33.9400534 C55.4782088,33.3533155 54.5267912,33.3533155 53.9398271,33.9402795 C53.3533532,34.5260034 53.3533532,35.4782465 53.9400534,36.0649466 Z" transform="translate(55.125000, 35.002111) rotate(-45.000000) translate(-55.125000, -35.002111) "></path>
<path d="M24.035,14.267 C24.035,13.8436424 23.6913576,13.5 23.267,13.5 C22.8435486,13.5 22.5,13.843736 22.5,14.267 C22.5,14.6911704 22.8434551,15.035 23.267,15.035 C23.6914513,15.035 24.035,14.6912641 24.035,14.267 Z M25.035,14.267 C25.035,15.2434269 24.2438581,16.035 23.267,16.035 C22.2909262,16.035 21.5,15.2432113 21.5,14.267 C21.5,13.2915732 22.2911418,12.5 23.267,12.5 C24.2436424,12.5 25.035,13.2913576 25.035,14.267 Z"></path>
<path d="M57.5,66.5 L57.5,66 C57.5,65.7238576 57.2761424,65.5 57,65.5 C56.7238576,65.5 56.5,65.7238576 56.5,66 L56.5,66.5 L56,66.5 C55.7238576,66.5 55.5,66.7238576 55.5,67 C55.5,67.2761424 55.7238576,67.5 56,67.5 L56.5,67.5 L56.5,68 C56.5,68.2761424 56.7238576,68.5 57,68.5 C57.2761424,68.5 57.5,68.2761424 57.5,68 L57.5,67.5 L58,67.5 C58.2761424,67.5 58.5,67.2761424 58.5,67 C58.5,66.7238576 58.2761424,66.5 58,66.5 L57.5,66.5 Z"></path>
<path d="M3.516,82.508 C3.516,81.9517457 3.06446111,81.5 2.508,81.5 C1.95214237,81.5 1.5,81.9521424 1.5,82.508 C1.5,83.0644611 1.95174573,83.516 2.508,83.516 C3.06485763,83.516 3.516,83.0648576 3.516,82.508 Z M4.516,82.508 C4.516,83.6171424 3.61714237,84.516 2.508,84.516 C1.39956348,84.516 0.5,83.6168484 0.5,82.508 C0.5,81.3998576 1.39985763,80.5 2.508,80.5 C3.61684841,80.5 4.516,81.3995635 4.516,82.508 Z"></path>
<path d="M6.89408522,42.8906809 C6.81018565,42.8447961 6.74120385,42.7758143 6.69531914,42.6919148 L5.793,41.0420367 L4.89068086,42.6919148 C4.84479615,42.7758143 4.77581435,42.8447961 4.69191478,42.8906809 L3.04203668,43.793 L4.69191478,44.6953191 C4.77581435,44.7412039 4.84479615,44.8101857 4.89068086,44.8940852 L5.793,46.5439633 L6.69531914,44.8940852 C6.74120385,44.8101857 6.81018565,44.7412039 6.89408522,44.6953191 L8.54396332,43.793 L6.89408522,42.8906809 Z M9.82591478,43.3543191 C10.1726951,43.5439735 10.1726951,44.0420265 9.82591478,44.2316809 L7.50240791,45.5024079 L6.23168086,47.8259148 C6.04202655,48.1726951 5.54397345,48.1726951 5.35431914,47.8259148 L4.08359209,45.5024079 L1.76008522,44.2316809 C1.41330493,44.0420265 1.41330493,43.5439735 1.76008522,43.3543191 L4.08359209,42.0835921 L5.35431914,39.7600852 C5.54397345,39.4133049 6.04202655,39.4133049 6.23168086,39.7600852 L7.50240791,42.0835921 L9.82591478,43.3543191 Z"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

View file

@ -57,3 +57,5 @@
@import "ui/ref";
@import "ui/monospaced";
@import 'ui/initialize-lfs';
@import 'ui/ci-status';
@import 'ui/pull-request-badge';

View file

@ -6,10 +6,9 @@
@import '~primer-support/lib/variables/color-system.scss';
:root {
--color-new: $green;
--color-deleted: $red;
--color-modified: $yellow;
--color-modified: $yellow-700;
--color-renamed: $blue;
--color-conflicted: $orange;
@ -160,7 +159,7 @@
--border-radius: 3px;
--base-border: 1px solid var(--box-border-color);
--shadow-color: rgba(71,83,95,0.19);
--shadow-color: rgba(71, 83, 95, 0.19);
--base-box-shadow: 0 2px 7px var(--shadow-color);
--toolbar-height: 50px;
@ -211,7 +210,6 @@
/** The highlight color used for focus rings and focus box shadows */
--focus-color: $blue;
/**
* Variables for form elements
*/

View file

@ -5,6 +5,10 @@
display: flex;
flex-direction: column;
& > .tab-bar {
border-top: var(--base-border);
}
.branches-list {
width: 300px;
}
@ -28,10 +32,12 @@
// padding on either side. Now in two flavors!
@include darwin {
height: 13px;
line-height: 13px;
}
@include win32 {
height: 14px;
line-height: 14px;
}
border-radius: 8px;
@ -45,6 +51,12 @@
.pull-request-list {
width: 300px;
.filter-list-filter-field {
// The rows have built-in margin to their content so
// we only need half a spacer here
padding-bottom: var(--spacing-half);
}
.pull-request-item {
padding: 0 var(--spacing);
display: flex;
@ -57,6 +69,10 @@
margin-right: var(--spacing-half);
width: 16px; // Force a consistent width
flex-shrink: 0;
// Align the icon baseline with the title text
align-self: flex-start;
margin-top: 2px;
}
.info {
@ -64,6 +80,7 @@
flex-direction: column;
min-width: 0;
flex-grow: 1;
margin-right: var(--spacing-half);
.title {
@include ellipsis;
@ -83,23 +100,6 @@
color: var(--text-secondary-color);
}
}
.status {
width: 16px;
flex-shrink: 0;
}
.status-pending {
color: $orange-700;
}
.status-failure {
color: $red-600;
}
.status-success {
color: $green-500;
}
}
.pull-request-loading-item {
@ -192,7 +192,8 @@
padding: var(--spacing);
.blankslate-image {
min-width: 300px;
width: 257px;
min-width: 0;
}
.title {

View file

@ -0,0 +1,16 @@
.ci-status {
height: 16px;
flex-shrink: 0;
}
.ci-status-pending {
color: $orange-700;
}
.ci-status-failure {
color: $red-500;
}
.ci-status-success {
color: $green-500;
}

View file

@ -0,0 +1,47 @@
.toolbar-dropdown.open .pr-badge {
background: $gray-300;
}
.toolbar-dropdown:not(.open) .pr-badge {
background: $gray-700;
}
.pr-badge {
--height: 18px;
display: flex;
flex-direction: row;
height: var(--height);
align-items: center;
border-radius: var(--border-radius);
border-width: 0;
margin-right: var(--spacing);
padding: var(--spacing-half);
.number {
font-size: var(--font-size-sm);
// We're explicitly providing line-height here so that the text gets
// properly vertically centered.
line-height: var(--height);
}
.ci-status {
margin-left: var(--spacing-half);
}
}
.toolbar-dropdown.open .ci-status-pending {
color: $orange-800;
}
.toolbar-dropdown.open .ci-status-failure {
color: $red-500;
}
.toolbar-dropdown.open .ci-status-success {
color: $green-700;
}

View file

@ -0,0 +1,62 @@
/* tslint:disable:no-sync-functions */
import * as path from 'path'
import { expect } from 'chai'
import { Repository } from '../../../src/models/repository'
import { reset, resetPaths, GitResetMode } from '../../../src/lib/git/reset'
import { getStatus } from '../../../src/lib/git/status'
import { setupFixtureRepository } from '../../fixture-helper'
import { GitProcess } from 'dugite'
import * as fs from 'fs-extra'
describe('git/reset', () => {
let repository: Repository | null = null
beforeEach(() => {
const testRepoPath = setupFixtureRepository('test-repo')
repository = new Repository(testRepoPath, -1, null, false)
})
describe('reset', () => {
it('can hard reset a repository', async () => {
const repoPath = repository!.path
const fileName = 'README.md'
const filePath = path.join(repoPath, fileName)
fs.writeFileSync(filePath, 'Hi world\n')
await reset(repository!, GitResetMode.Hard, 'HEAD')
const status = await getStatus(repository!)
expect(status.workingDirectory.files.length).to.equal(0)
})
})
describe('resetPaths', () => {
it('resets discarded staged file', async () => {
const repoPath = repository!.path
const fileName = 'README.md'
const filePath = path.join(repoPath, fileName)
// modify the file
fs.writeFileSync(filePath, 'Hi world\n')
// stage the file, then delete it to mimic discarding
GitProcess.exec(['add', fileName], repoPath)
fs.unlinkSync(filePath)
await resetPaths(repository!, GitResetMode.Mixed, 'HEAD', [filePath])
// then checkout the version from the index to restore it
await GitProcess.exec(
['checkout-index', '-f', '-u', '-q', '--', fileName],
repoPath
)
const status = await getStatus(repository!)
expect(status.workingDirectory.files.length).to.equal(0)
})
})
})

View file

@ -226,9 +226,9 @@ devtron@^1.4.0:
highlight.js "^9.3.0"
humanize-plus "^1.8.1"
dexie@^1.4.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/dexie/-/dexie-1.5.1.tgz#ac3ad5a0ebaf7e6e42760db58710418d4a756624"
dexie@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dexie/-/dexie-2.0.0.tgz#d3ad7b7aa29e8de82ea4a8e09a4ccbfa80213f0d"
dom-classlist@^1.0.1:
version "1.0.1"
@ -242,9 +242,9 @@ dom-matches@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dom-matches/-/dom-matches-2.0.0.tgz#d2728b416a87533980eb089b848d253cf23a758c"
dugite@^1.43.0:
version "1.43.0"
resolved "https://registry.yarnpkg.com/dugite/-/dugite-1.43.0.tgz#48de4a783e243a7a55021fdc419a9c99912f776a"
dugite@^1.45.0:
version "1.45.0"
resolved "https://registry.yarnpkg.com/dugite/-/dugite-1.45.0.tgz#9c770e1e992eb2b5de9e8de3353a8c11ff3b3a81"
dependencies:
checksum "^0.1.1"
mkdirp "^0.5.1"
@ -733,20 +733,20 @@ react-addons-perf@15.4.2:
fbjs "^0.8.4"
object-assign "^4.1.0"
react-addons-shallow-compare@^15.6.0:
version "15.6.0"
resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.0.tgz#b7a4e5ff9f2704c20cf686dd8a05dd08b26de252"
react-addons-shallow-compare@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.2.tgz#198a00b91fc37623db64a28fd17b596ba362702f"
dependencies:
fbjs "^0.8.4"
object-assign "^4.1.0"
react-addons-test-utils@^15.4.2:
version "15.6.0"
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.0.tgz#062d36117fe8d18f3ba5e06eb33383b0b85ea5b9"
react-addons-test-utils@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156"
react-dom@^15.6.1:
version "15.6.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.1.tgz#2cb0ed4191038e53c209eb3a79a23e2a4cf99470"
react-dom@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730"
dependencies:
fbjs "^0.8.9"
loose-envify "^1.1.0"
@ -763,7 +763,7 @@ react-transition-group@^1.2.0:
prop-types "^15.5.6"
warning "^3.0.0"
react-virtualized@^9.8.0:
react-virtualized@^9.10.1:
version "9.10.1"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.10.1.tgz#d32365d0edf49debbe25fbfe73b5f55f6d9d8c72"
dependencies:
@ -773,9 +773,9 @@ react-virtualized@^9.8.0:
loose-envify "^1.3.0"
prop-types "^15.5.4"
react@^15.6.1:
version "15.6.1"
resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df"
react@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
dependencies:
create-react-class "^15.6.0"
fbjs "^0.8.9"

View file

@ -1,5 +1,48 @@
{
"releases": {
"1.0.4-beta0": [
"[Improved] Increase the contrast of the modified file status octicons - #2914",
"[Fixed] Showing changed files in Finder/Explorer would open the file - #2909",
"[Fixed] macOS: Fix app icon on High Sierra - #2915",
"[Fixed] Cloning an empty repository would fail - #2897 #2906",
"[Fixed] Catch logging exceptions - #2910"
],
"1.0.3": [
"[Improved] Increase the contrast of the modified file status octicons - #2914",
"[Fixed] Showing changed files in Finder/Explorer would open the file - #2909",
"[Fixed] macOS: Fix app icon on High Sierra - #2915",
"[Fixed] Cloning an empty repository would fail - #2897 #2906",
"[Fixed] Catch logging exceptions - #2910"
],
"1.0.2": [
"[Improved] Better message for GitHub Enterprise users when there is a network error - #2574. Thanks @agisilaos!",
"[Improved] Clone error message now suggests networking might be involved - #2872. Thanks @agisilaos!",
"[Improved] Include push/pull progress information in the push/pull button tooltip - #2879",
"[Improved] Allow publishing a brand new, empty repository - #2773",
"[Improved] Make file paths in lists selectable - #2801. Thanks @artivilla!",
"[Fixed] Disable LFS hook creation when cloning - #2809",
"[Fixed] Use the new URL for the \"Show User Guides\" menu item - #2792. Thanks @db6edr!",
"[Fixed] Make the SHA selectable when viewing commit details - #1154",
"[Fixed] Windows: Make `github` CLI work in Git Bash - #2712",
"[Fixed] Use the initial path provided when creating a new repository - #2883",
"[Fixed] Windows: Avoid long path limits when discarding changes - #2833",
"[Fixed] Files would get deleted when undoing the first commit - #2764",
"[Fixed] Find the repository root before adding it - #2832",
"[Fixed] Display warning about an existing folder before cloning - #2777 #2830",
"[Fixed] Show contents of directory when showing a repository from Show in Explorer/Finder instead of showing the parent - #2798"
],
"1.0.2-beta1": [
"[Improved] Clone error message now suggests networking might be involved - #2872. Thanks @agisilaos!",
"[Improved] Include push/pull progress information in the push/pull button tooltip - #2879",
"[Improved] Allow publishing a brand new, empty repository - #2773",
"[Improved] Make file paths in lists selectable - #2801. Thanks @artivilla!",
"[Fixed] Use the initial path provided when creating a new repository - #2883",
"[Fixed] Windows: Avoid long path limits when discarding changes - #2833",
"[Fixed] Files would get deleted when undoing the first commit - #2764",
"[Fixed] Find the repository root before adding it - #2832",
"[Fixed] Display warning about an existing folder before cloning - #2777 #2830",
"[Fixed] Show contents of directory when showing a repository from Show in Explorer/Finder instead of showing the parent - #2798"
],
"1.0.2-beta0": [
"[Improved] Message for GitHub Enterprise users when there is a network error - #2574. Thanks @agisilaos!",
"[Fixed] Disable LFS hook creation when cloning - #2809",

View file

@ -19,9 +19,10 @@
"build:dev": "npm run compile:dev && cross-env NODE_ENV=development ts-node script/build.ts",
"build:prod": "npm run compile:prod && cross-env NODE_ENV=production ts-node script/build.ts",
"package": "node script/package",
"generate-octicons": "ts-node script/generate-octicons.ts",
"clean:tslint": "rimraf tslint-rules/*.js",
"compile:tslint": "tsc -p tslint-rules",
"lint": "npm run compile:tslint && tslint \"./app/{src,test}/**/*.{ts,tsx}\"",
"lint": "npm run compile:tslint && tslint \"./app/{src,test}/**/*.{ts,tsx}\" \"./scripts/*.ts\"",
"check-prettiness": "node script/is-it-pretty",
"publish": "node script/publish",
"clean-slate": "rimraf out node_modules app/node_modules && npm install",
@ -97,15 +98,17 @@
"@types/keytar": "^4.0.0",
"@types/mocha": "^2.2.29",
"@types/node": "^7.0.18",
"@types/react": "15.0.31",
"@types/react-addons-test-utils": "0.14.19",
"@types/react-dom": "15.5.1",
"@types/react": "^15.6.4",
"@types/react-addons-test-utils": "^0.14.20",
"@types/react-dom": "^15.5.5",
"@types/react-transition-group": "1.1.1",
"@types/react-virtualized": "9.7.2",
"@types/react-virtualized": "^9.7.4",
"@types/temp": "^0.8.29",
"@types/to-camel-case": "^1.0.0",
"@types/ua-parser-js": "^0.7.30",
"@types/uuid": "^3.4.0",
"@types/winston": "^2.2.0",
"@types/xml2js": "^0.4.0",
"prettier": "~1.7.0",
"tslint-config-prettier": "^1.1.0",
"tslint-react": "~3.0.0"

View file

@ -1,5 +1,3 @@
#!/usr/bin/env node
/* generate-octicons
*
* Utility script for generating a strongly typed representation of all
@ -7,13 +5,10 @@
* downloads the latest version of the `sprite.octicons.svg` file.
*/
'use strict'
const fs = require('fs')
const process = require('process')
const xml2js = require('xml2js')
const path = require('path')
const toCamelCase = require('to-camel-case')
import fs = require('fs')
import xml2js = require('xml2js')
import path = require('path')
import toCamelCase = require('to-camel-case')
const filePath = path.resolve(
__dirname,
@ -24,12 +19,24 @@ const filePath = path.resolve(
'sprite.octicons.svg'
)
const file = fs.readFileSync(filePath)
/* tslint:disable:no-sync-functions */
const file = fs.readFileSync(filePath, 'utf-8')
xml2js.parseString(file, function(err, result) {
interface IXML2JSResult {
svg: { symbol: ReadonlyArray<IXML2JSNode> }
}
interface IXML2JSNode {
$: { [key: string]: string }
path: ReadonlyArray<IXML2JSNode>
}
xml2js.parseString(file, function(err, result: IXML2JSResult) {
const viewBoxRe = /0 0 (\d+) (\d+)/
const out = fs.createWriteStream(
path.resolve(__dirname, '../app/src/ui/octicons/octicons.generated.ts')
path.resolve(__dirname, '../app/src/ui/octicons/octicons.generated.ts'),
{
encoding: 'utf-8',
}
)
out.write('/*\n')
@ -55,8 +62,9 @@ xml2js.parseString(file, function(err, result) {
const viewBoxMatch = viewBoxRe.exec(viewBox)
if (!viewBoxMatch) {
console.error(`Unexpected viewBox format for ${id}`)
process.exit(1)
console.error(`*** ERROR! Unexpected viewBox format for ${id}`)
process.exitCode = 1
return
}
const [, w, h] = viewBoxMatch

View file

@ -48,15 +48,15 @@
version "7.0.43"
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c"
"@types/react-addons-test-utils@0.14.19":
version "0.14.19"
resolved "https://registry.yarnpkg.com/@types/react-addons-test-utils/-/react-addons-test-utils-0.14.19.tgz#260f60adb2015cc11938c01a9c4015323a3610e2"
"@types/react-addons-test-utils@^0.14.20":
version "0.14.20"
resolved "https://registry.yarnpkg.com/@types/react-addons-test-utils/-/react-addons-test-utils-0.14.20.tgz#66a5787b8f15cce01c195fe2b95b89046054810d"
dependencies:
"@types/react" "*"
"@types/react-dom@15.5.1":
version "15.5.1"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-15.5.1.tgz#f3c3e14c682785923c7d64583537df319442dec1"
"@types/react-dom@^15.5.5":
version "15.5.5"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-15.5.5.tgz#6b117c7697b61fe74132bfe5c72bceb3319433b8"
dependencies:
"@types/react" "*"
@ -66,15 +66,15 @@
dependencies:
"@types/react" "*"
"@types/react-virtualized@9.7.2":
version "9.7.2"
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.7.2.tgz#0ddc40f0c82cf859a0bec329e99b7b157e6d400d"
"@types/react-virtualized@^9.7.4":
version "9.7.4"
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.7.4.tgz#c8f8ab729abca03fa76deb0eef820a5daee22129"
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@15.0.31":
version "15.0.31"
resolved "https://registry.yarnpkg.com/@types/react/-/react-15.0.31.tgz#21dfc5d41ee1600ff7d7b738ad21a9502aa3ecf2"
"@types/react@15.6.4", "@types/react@^15.6.4":
version "15.6.4"
resolved "https://registry.yarnpkg.com/@types/react/-/react-15.6.4.tgz#3bb57bd43183a05919ceb025a264287348f47e9d"
"@types/temp@^0.8.29":
version "0.8.29"
@ -82,6 +82,10 @@
dependencies:
"@types/node" "*"
"@types/to-camel-case@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/to-camel-case/-/to-camel-case-1.0.0.tgz#927ef0a7294d90b1835466c29b64b8ad2a32d8b5"
"@types/ua-parser-js@^0.7.30":
version "0.7.32"
resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.32.tgz#8827d451d6702307248073b5d98aa9293d02b5e5"
@ -98,6 +102,12 @@
dependencies:
"@types/node" "*"
"@types/xml2js@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.0.tgz#e54a89a0055d5ed69305b2610f970909bf363e45"
dependencies:
"@types/node" "*"
abbrev@1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"