mirror of
https://github.com/desktop/desktop
synced 2024-08-27 04:00:55 +00:00
Merge branch 'master' into my-hands-are-typing
This commit is contained in:
commit
72ea02412c
|
@ -74,10 +74,7 @@ rules:
|
|||
###########
|
||||
prettier/prettier:
|
||||
- error
|
||||
- singleQuote: true
|
||||
trailingComma: es5
|
||||
semi: false
|
||||
parser: typescript
|
||||
- parser: typescript
|
||||
no-restricted-syntax:
|
||||
- error
|
||||
# no-default-export
|
||||
|
|
20
.github/ISSUE_TEMPLATE.md
vendored
20
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,25 +1,5 @@
|
|||
<!--
|
||||
|
||||
Have you read GitHub Desktop's Code of Conduct? By filing an Issue, you are
|
||||
expected to comply with it, including treating everyone with respect:
|
||||
|
||||
https://github.com/desktop/desktop/blob/master/CODE_OF_CONDUCT.md
|
||||
|
||||
-->
|
||||
|
||||
<!--
|
||||
|
||||
Are you encountering an issue where the “Minimize” tooltip stays visible
|
||||
when you click the minimize button in the window? If so, that is an issue
|
||||
with Electron, the framework the app uses. Please subscribe to the issue
|
||||
at this link for updates on the issue:
|
||||
|
||||
https://github.com/electron/electron/issues/9943
|
||||
|
||||
-->
|
||||
|
||||
<!--
|
||||
|
||||
Please summarize the issue in the title, and then use the template below to
|
||||
fill out the details so we can reproduce the issue on our end.
|
||||
|
||||
|
|
|
@ -1,2 +1,16 @@
|
|||
out/
|
||||
dist/
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
app/node_modules/
|
||||
.DS_Store
|
||||
.awcache
|
||||
.idea/
|
||||
.eslintcache
|
||||
|
||||
app/static/common
|
||||
app/test/fixtures
|
||||
gemoji
|
||||
*.json
|
||||
*.md
|
||||
|
|
4
.prettierrc.yml
Normal file
4
.prettierrc.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
singleQuote: true
|
||||
trailingComma: es5
|
||||
semi: false
|
||||
proseWrap: always
|
|
@ -57,10 +57,7 @@ install:
|
|||
- yarn install --force
|
||||
|
||||
script:
|
||||
- yarn lint
|
||||
- yarn build:prod
|
||||
- yarn test:setup
|
||||
- yarn test
|
||||
- yarn lint && yarn build:prod && yarn test:setup && yarn test
|
||||
|
||||
after_failure:
|
||||
- yarn test:review
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"productName": "GitHub Desktop",
|
||||
"bundleID": "com.github.GitHubClient",
|
||||
"companyName": "GitHub, Inc.",
|
||||
"version": "1.0.14-beta1",
|
||||
"version": "1.0.14-beta4",
|
||||
"main": "./main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -22,6 +22,7 @@
|
|||
"chalk": "^2.3.0",
|
||||
"classnames": "^2.2.5",
|
||||
"codemirror": "^5.31.0",
|
||||
"codemirror-mode-elixir": "1.1.1",
|
||||
"deep-equal": "^1.0.1",
|
||||
"dexie": "^2.0.0",
|
||||
"dugite": "1.57.0",
|
||||
|
@ -31,6 +32,7 @@
|
|||
"file-url": "^2.0.2",
|
||||
"front-matter": "^2.1.2",
|
||||
"fs-extra": "^2.1.2",
|
||||
"fuzzaldrin-plus": "^0.6.0",
|
||||
"keytar": "^4.0.4",
|
||||
"moment": "^2.17.1",
|
||||
"mri": "^1.1.0",
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
@import '../../../styles/ui/button';
|
||||
@import '../../../styles/ui/scroll';
|
||||
|
||||
#desktop-crash-container, #crash-app {
|
||||
#desktop-crash-container,
|
||||
#crash-app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
@ -35,7 +36,6 @@ pre.error {
|
|||
}
|
||||
|
||||
header {
|
||||
|
||||
margin-bottom: var(--spacing-double);
|
||||
display: flex;
|
||||
flex: none;
|
||||
|
|
|
@ -108,6 +108,10 @@ extensionMIMEMap.set('.edn', 'text/x-clojure')
|
|||
import 'codemirror/mode/rust/rust'
|
||||
extensionMIMEMap.set('.rs', 'text/x-rustsrc')
|
||||
|
||||
import 'codemirror-mode-elixir'
|
||||
extensionMIMEMap.set('.ex', 'text/x-elixir')
|
||||
extensionMIMEMap.set('.exs', 'text/x-elixir')
|
||||
|
||||
function guessMimeType(contents: string) {
|
||||
if (contents.startsWith('<?xml')) {
|
||||
return 'text/xml'
|
||||
|
|
|
@ -80,12 +80,6 @@ export interface IAPIUser {
|
|||
readonly type: 'User' | 'Organization'
|
||||
}
|
||||
|
||||
/**
|
||||
* An expression that validates a GitHub.com or GitHub Enterprise
|
||||
* username
|
||||
*/
|
||||
export const validLoginExpression = /^[a-z0-9]+(-[a-z0-9]+)*$/i
|
||||
|
||||
/** The users we get from the mentionables endpoint. */
|
||||
export interface IAPIMentionableUser {
|
||||
readonly avatar_url: string
|
||||
|
@ -567,7 +561,10 @@ export class API {
|
|||
*/
|
||||
public async fetchUser(login: string): Promise<IAPIUser | null> {
|
||||
try {
|
||||
const response = await this.request('GET', `users/${login}`)
|
||||
const response = await this.request(
|
||||
'GET',
|
||||
`users/${encodeURIComponent(login)}`
|
||||
)
|
||||
|
||||
if (response.status === 404) {
|
||||
return null
|
||||
|
|
|
@ -13,9 +13,14 @@
|
|||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators
|
||||
*
|
||||
* This method can be chained using `||` for more complex sorting
|
||||
* logic. Ex
|
||||
* logic. Example:
|
||||
*
|
||||
* arr.sort((x, y) => compare(x.firstName, y.firstName) || compare(x.lastName, y.lastName))
|
||||
* ```ts
|
||||
* arr.sort(
|
||||
* (x, y) =>
|
||||
* compare(x.firstName, y.firstName) || compare(x.lastName, y.lastName)
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
export function compare<T>(x: T, y: T): number {
|
||||
|
|
38
app/src/lib/databases/base-database.ts
Normal file
38
app/src/lib/databases/base-database.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import Dexie from 'dexie'
|
||||
|
||||
export abstract class BaseDatabase extends Dexie {
|
||||
private schemaVersion: number | undefined
|
||||
|
||||
public constructor(name: string, schemaVersion: number | undefined) {
|
||||
super(name)
|
||||
|
||||
this.schemaVersion = schemaVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the version of the schema only if `targetVersion` is less than
|
||||
* `version` or is `undefined`.
|
||||
*
|
||||
* targetVersion - The version of the schema that is being targetted. If not
|
||||
* provided, the given version will be registered.
|
||||
* version - The version being registered.
|
||||
* schema - The schema to register.
|
||||
* upgrade - An upgrade function to call after upgrading to the given
|
||||
* version.
|
||||
*/
|
||||
protected async conditionalVersion(
|
||||
version: number,
|
||||
schema: { [key: string]: string | null },
|
||||
upgrade?: (t: Dexie.Transaction) => Promise<void>
|
||||
) {
|
||||
if (this.schemaVersion != null && this.schemaVersion < version) {
|
||||
return
|
||||
}
|
||||
|
||||
const dexieVersion = this.version(version).stores(schema)
|
||||
|
||||
if (upgrade != null) {
|
||||
await dexieVersion.upgrade(upgrade)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
import Dexie from 'dexie'
|
||||
|
||||
// NB: This _must_ be incremented whenever the DB key scheme changes.
|
||||
const DatabaseVersion = 2
|
||||
import { BaseDatabase } from './base-database'
|
||||
|
||||
export interface IGitHubUser {
|
||||
/**
|
||||
|
@ -31,18 +29,18 @@ export interface IMentionableAssociation {
|
|||
readonly repositoryID: number
|
||||
}
|
||||
|
||||
export class GitHubUserDatabase extends Dexie {
|
||||
export class GitHubUserDatabase extends BaseDatabase {
|
||||
public users: Dexie.Table<IGitHubUser, number>
|
||||
public mentionables: Dexie.Table<IMentionableAssociation, number>
|
||||
|
||||
public constructor(name: string) {
|
||||
super(name)
|
||||
public constructor(name: string, schemaVersion?: number) {
|
||||
super(name, schemaVersion)
|
||||
|
||||
this.version(1).stores({
|
||||
this.conditionalVersion(1, {
|
||||
users: '++id, &[endpoint+email]',
|
||||
})
|
||||
|
||||
this.version(DatabaseVersion).stores({
|
||||
this.conditionalVersion(2, {
|
||||
users: '++id, [endpoint+email], [endpoint+login]',
|
||||
mentionables: '++id, repositoryID, &[userID+repositoryID]',
|
||||
})
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import Dexie from 'dexie'
|
||||
|
||||
// NB: This _must_ be incremented whenever the DB key scheme changes.
|
||||
const DatabaseVersion = 2
|
||||
import { BaseDatabase } from './base-database'
|
||||
|
||||
export interface IIssue {
|
||||
readonly id?: number
|
||||
|
@ -11,41 +9,49 @@ export interface IIssue {
|
|||
readonly updated_at?: string
|
||||
}
|
||||
|
||||
export class IssuesDatabase extends Dexie {
|
||||
export class IssuesDatabase extends BaseDatabase {
|
||||
public issues: Dexie.Table<IIssue, number>
|
||||
|
||||
public constructor(name: string) {
|
||||
super(name)
|
||||
public constructor(name: string, schemaVersion?: number) {
|
||||
super(name, schemaVersion)
|
||||
|
||||
this.version(1).stores({
|
||||
this.conditionalVersion(1, {
|
||||
issues: '++id, &[gitHubRepositoryID+number], gitHubRepositoryID, number',
|
||||
})
|
||||
|
||||
this.version(DatabaseVersion)
|
||||
.stores({
|
||||
this.conditionalVersion(
|
||||
2,
|
||||
{
|
||||
issues:
|
||||
'++id, &[gitHubRepositoryID+number], gitHubRepositoryID, number, [gitHubRepositoryID+updated_at]',
|
||||
})
|
||||
.upgrade(t => {
|
||||
// Clear deprecated localStorage keys, we compute the since parameter
|
||||
// using the database now.
|
||||
Object.keys(localStorage)
|
||||
.filter(key => /^IssuesStore\/\d+\/lastFetch$/.test(key))
|
||||
.forEach(key => localStorage.removeItem(key))
|
||||
|
||||
// Unfortunately we have to clear the issues in order to maintain
|
||||
// data consistency in the database. The issues table is only supposed
|
||||
// to store 'open' issues and if we kept the existing issues (which)
|
||||
// don't have an updated_at field around the initial query for
|
||||
// max(updated_at) would return null, causing us to fetch all _open_
|
||||
// issues which in turn means we wouldn't be able to detect if we
|
||||
// have any issues in the database that have been closed since the
|
||||
// last time we fetched. Not only that, these closed issues wouldn't
|
||||
// be updated to include the updated_at field unless they were actually
|
||||
// modified at a later date.
|
||||
//
|
||||
// TL;DR; This is the safest approach
|
||||
return t.table('issues').clear()
|
||||
})
|
||||
},
|
||||
clearIssues
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function clearIssues(transaction: Dexie.Transaction) {
|
||||
// Clear deprecated localStorage keys, we compute the since parameter
|
||||
// using the database now.
|
||||
clearDeprecatedKeys()
|
||||
|
||||
// Unfortunately we have to clear the issues in order to maintain
|
||||
// data consistency in the database. The issues table is only supposed
|
||||
// to store 'open' issues and if we kept the existing issues (which)
|
||||
// don't have an updated_at field around the initial query for
|
||||
// max(updated_at) would return null, causing us to fetch all _open_
|
||||
// issues which in turn means we wouldn't be able to detect if we
|
||||
// have any issues in the database that have been closed since the
|
||||
// last time we fetched. Not only that, these closed issues wouldn't
|
||||
// be updated to include the updated_at field unless they were actually
|
||||
// modified at a later date.
|
||||
//
|
||||
// TL;DR; This is the safest approach
|
||||
return transaction.table('issues').clear()
|
||||
}
|
||||
|
||||
function clearDeprecatedKeys() {
|
||||
Object.keys(localStorage)
|
||||
.filter(key => /^IssuesStore\/\d+\/lastFetch$/.test(key))
|
||||
.forEach(key => localStorage.removeItem(key))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Dexie from 'dexie'
|
||||
import { APIRefState, IAPIRefStatusItem } from '../api'
|
||||
import { BaseDatabase } from './base-database'
|
||||
|
||||
export interface IPullRequestRef {
|
||||
/**
|
||||
|
@ -65,26 +66,47 @@ export interface IPullRequestStatus {
|
|||
* if the database object was created prior to status support
|
||||
* being added in #3588
|
||||
*/
|
||||
readonly statuses: ReadonlyArray<IAPIRefStatusItem> | undefined
|
||||
readonly statuses?: ReadonlyArray<IAPIRefStatusItem>
|
||||
}
|
||||
|
||||
export class PullRequestDatabase extends Dexie {
|
||||
export class PullRequestDatabase extends BaseDatabase {
|
||||
public pullRequests: Dexie.Table<IPullRequest, number>
|
||||
public pullRequestStatus: Dexie.Table<IPullRequestStatus, number>
|
||||
|
||||
public constructor(name: string) {
|
||||
super(name)
|
||||
public constructor(name: string, schemaVersion?: number) {
|
||||
super(name, schemaVersion)
|
||||
|
||||
this.version(1).stores({
|
||||
this.conditionalVersion(1, {
|
||||
pullRequests: 'id++, base.repoId',
|
||||
})
|
||||
|
||||
this.version(2).stores({
|
||||
this.conditionalVersion(2, {
|
||||
pullRequestStatus: 'id++, &[sha+pullRequestId]',
|
||||
})
|
||||
|
||||
this.version(3).stores({
|
||||
this.conditionalVersion(3, {
|
||||
pullRequestStatus: 'id++, &[sha+pullRequestId], pullRequestId',
|
||||
})
|
||||
|
||||
// we need to run the upgrade function to ensure we add
|
||||
// a status field to all previous records
|
||||
this.conditionalVersion(4, {}, this.addStatusesField)
|
||||
}
|
||||
|
||||
private addStatusesField = async (transaction: Dexie.Transaction) => {
|
||||
const table = this.pullRequestStatus
|
||||
|
||||
await table.toCollection().modify(async prStatus => {
|
||||
if (prStatus.statuses == null) {
|
||||
const newPrStatus = { statuses: [], ...prStatus }
|
||||
|
||||
await table
|
||||
.where('[sha+pullRequestId]')
|
||||
.equals([prStatus.sha, prStatus.pullRequestId])
|
||||
.delete()
|
||||
|
||||
await table.add(newPrStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Dexie from 'dexie'
|
||||
import { BaseDatabase } from './base-database'
|
||||
|
||||
export interface IDatabaseOwner {
|
||||
readonly id?: number | null
|
||||
|
@ -27,7 +28,7 @@ export interface IDatabaseRepository {
|
|||
}
|
||||
|
||||
/** The repositories database. */
|
||||
export class RepositoriesDatabase extends Dexie {
|
||||
export class RepositoriesDatabase extends BaseDatabase {
|
||||
/** The local repositories table. */
|
||||
public repositories: Dexie.Table<IDatabaseRepository, number>
|
||||
|
||||
|
@ -45,15 +46,15 @@ export class RepositoriesDatabase extends Dexie {
|
|||
* database will be created with the latest version.
|
||||
*/
|
||||
public constructor(name: string, schemaVersion?: number) {
|
||||
super(name)
|
||||
super(name, schemaVersion)
|
||||
|
||||
this.conditionalVersion(schemaVersion, 1, {
|
||||
this.conditionalVersion(1, {
|
||||
repositories: '++id, &path',
|
||||
gitHubRepositories: '++id, name',
|
||||
owners: '++id, login',
|
||||
})
|
||||
|
||||
this.conditionalVersion(schemaVersion, 2, {
|
||||
this.conditionalVersion(2, {
|
||||
owners: '++id, &[endpoint+login]',
|
||||
})
|
||||
|
||||
|
@ -61,48 +62,16 @@ export class RepositoriesDatabase extends Dexie {
|
|||
// version and its upgrade callback only happens *after* the schema's been
|
||||
// changed. So we need to prepare for it by removing any old data now
|
||||
// which will violate it.
|
||||
this.conditionalVersion(
|
||||
schemaVersion,
|
||||
3,
|
||||
{},
|
||||
removeDuplicateGitHubRepositories
|
||||
)
|
||||
this.conditionalVersion(3, {}, removeDuplicateGitHubRepositories)
|
||||
|
||||
this.conditionalVersion(schemaVersion, 4, {
|
||||
this.conditionalVersion(4, {
|
||||
gitHubRepositories: '++id, name, &[ownerID+name]',
|
||||
})
|
||||
|
||||
this.conditionalVersion(schemaVersion, 5, {
|
||||
this.conditionalVersion(5, {
|
||||
gitHubRepositories: '++id, name, &[ownerID+name], cloneURL',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the version of the schema only if `targetVersion` is less than
|
||||
* `version` or is `undefined`.
|
||||
*
|
||||
* targetVersion - The version of the schema that is being targetted. If not
|
||||
* provided, the given version will be registered.
|
||||
* version - The version being registered.
|
||||
* schema - The schema to register.
|
||||
* upgrade - An upgrade function to call after upgrading to the given
|
||||
* version.
|
||||
*/
|
||||
private conditionalVersion(
|
||||
targetVersion: number | undefined,
|
||||
version: number,
|
||||
schema: { [key: string]: string | null },
|
||||
upgrade?: (t: Dexie.Transaction) => void
|
||||
) {
|
||||
if (targetVersion && targetVersion < version) {
|
||||
return
|
||||
}
|
||||
|
||||
const dexieVersion = this.version(version).stores(schema)
|
||||
if (upgrade) {
|
||||
dexieVersion.upgrade(upgrade)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,6 +89,7 @@ function removeDuplicateGitHubRepositories(transaction: Dexie.Transaction) {
|
|||
// We can be sure `id` isn't null since we just got it from the
|
||||
// database.
|
||||
const id = repo.id!
|
||||
|
||||
table.delete(id)
|
||||
} else {
|
||||
seenKeys.add(key)
|
||||
|
|
|
@ -1091,11 +1091,6 @@ export class Dispatcher {
|
|||
return this.appStore._openCreatePullRequestInBrowser(repository, branch)
|
||||
}
|
||||
|
||||
/** Refresh the list of open pull requests for the repository. */
|
||||
public refreshPullRequests(repository: Repository): Promise<void> {
|
||||
return this.appStore._refreshPullRequests(repository)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the existing `upstream` remote to point to the repository's parent.
|
||||
*/
|
||||
|
|
|
@ -10,6 +10,8 @@ export enum ExternalEditor {
|
|||
SublimeText = 'Sublime Text',
|
||||
BBEdit = 'BBEdit',
|
||||
PhpStorm = 'PhpStorm',
|
||||
RubyMine = 'RubyMine',
|
||||
TextMate = 'TextMate',
|
||||
}
|
||||
|
||||
export function parse(label: string): ExternalEditor | null {
|
||||
|
@ -31,7 +33,12 @@ export function parse(label: string): ExternalEditor | null {
|
|||
if (label === ExternalEditor.PhpStorm) {
|
||||
return ExternalEditor.PhpStorm
|
||||
}
|
||||
|
||||
if (label === ExternalEditor.RubyMine) {
|
||||
return ExternalEditor.RubyMine
|
||||
}
|
||||
if (label === ExternalEditor.TextMate) {
|
||||
return ExternalEditor.TextMate
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -54,6 +61,10 @@ function getBundleIdentifiers(editor: ExternalEditor): ReadonlyArray<string> {
|
|||
return ['com.barebones.bbedit']
|
||||
case ExternalEditor.PhpStorm:
|
||||
return ['com.jetbrains.PhpStorm']
|
||||
case ExternalEditor.RubyMine:
|
||||
return ['com.jetbrains.RubyMine']
|
||||
case ExternalEditor.TextMate:
|
||||
return ['com.macromates.TextMate']
|
||||
default:
|
||||
return assertNever(editor, `Unknown external editor: ${editor}`)
|
||||
}
|
||||
|
@ -82,6 +93,10 @@ function getExecutableShim(
|
|||
return Path.join(installPath, 'Contents', 'Helpers', 'bbedit_tool')
|
||||
case ExternalEditor.PhpStorm:
|
||||
return Path.join(installPath, 'Contents', 'MacOS', 'phpstorm')
|
||||
case ExternalEditor.RubyMine:
|
||||
return Path.join(installPath, 'Contents', 'MacOS', 'rubymine')
|
||||
case ExternalEditor.TextMate:
|
||||
return Path.join(installPath, 'Contents', 'Resources', 'mate')
|
||||
default:
|
||||
return assertNever(editor, `Unknown external editor: ${editor}`)
|
||||
}
|
||||
|
@ -123,6 +138,8 @@ export async function getAvailableEditors(): Promise<
|
|||
sublimePath,
|
||||
bbeditPath,
|
||||
phpStormPath,
|
||||
rubyMinePath,
|
||||
textMatePath,
|
||||
] = await Promise.all([
|
||||
findApplication(ExternalEditor.Atom),
|
||||
findApplication(ExternalEditor.VisualStudioCode),
|
||||
|
@ -130,6 +147,8 @@ export async function getAvailableEditors(): Promise<
|
|||
findApplication(ExternalEditor.SublimeText),
|
||||
findApplication(ExternalEditor.BBEdit),
|
||||
findApplication(ExternalEditor.PhpStorm),
|
||||
findApplication(ExternalEditor.RubyMine),
|
||||
findApplication(ExternalEditor.TextMate),
|
||||
])
|
||||
|
||||
if (atomPath) {
|
||||
|
@ -159,5 +178,13 @@ export async function getAvailableEditors(): Promise<
|
|||
results.push({ editor: ExternalEditor.PhpStorm, path: phpStormPath })
|
||||
}
|
||||
|
||||
if (rubyMinePath) {
|
||||
results.push({ editor: ExternalEditor.RubyMine, path: rubyMinePath })
|
||||
}
|
||||
|
||||
if (textMatePath) {
|
||||
results.push({ editor: ExternalEditor.TextMate, path: textMatePath })
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
|
47
app/src/lib/fuzzy-find.ts
Normal file
47
app/src/lib/fuzzy-find.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import * as fuzzAldrin from 'fuzzaldrin-plus'
|
||||
|
||||
import { compareDescending } from './compare'
|
||||
|
||||
const options: fuzzAldrin.IFilterOptions = {
|
||||
allowErrors: true,
|
||||
isPath: true,
|
||||
pathSeparator: '-',
|
||||
}
|
||||
|
||||
function score(str: string, query: string, maxScore: number) {
|
||||
return fuzzAldrin.score(str, query, undefined, options) / maxScore
|
||||
}
|
||||
|
||||
export interface IMatch<T> {
|
||||
/** `0 <= score <= 1` */
|
||||
score: number
|
||||
item: T
|
||||
matches: ReadonlyArray<number>
|
||||
}
|
||||
|
||||
export type KeyFunction<T> = (item: T) => string
|
||||
|
||||
export function match<T, _K extends keyof T>(
|
||||
query: string,
|
||||
items: ReadonlyArray<T>,
|
||||
getKey: _K | KeyFunction<T>
|
||||
): ReadonlyArray<IMatch<T>> {
|
||||
// matching `query` against itself is a perfect match.
|
||||
const maxScore = score(query, query, 1)
|
||||
const result = items
|
||||
.map((item): IMatch<T> => {
|
||||
const key: string =
|
||||
typeof getKey === 'function'
|
||||
? (getKey as KeyFunction<T>)(item)
|
||||
: String(item[getKey])
|
||||
|
||||
return {
|
||||
score: score(key, query, maxScore),
|
||||
item,
|
||||
matches: fuzzAldrin.match(key, query, undefined, options),
|
||||
}
|
||||
})
|
||||
.filter(({ matches }) => matches.length > 0)
|
||||
.sort(({ score: left }, { score: right }) => compareDescending(left, right))
|
||||
return result
|
||||
}
|
|
@ -101,6 +101,10 @@ export async function getCommitDiff(
|
|||
file.path,
|
||||
]
|
||||
|
||||
if (file.oldPath != null) {
|
||||
args.push(file.oldPath)
|
||||
}
|
||||
|
||||
const { output } = await spawnAndComplete(
|
||||
args,
|
||||
repository.path,
|
||||
|
|
|
@ -11,9 +11,17 @@ export async function listSubmodules(
|
|||
const result = await git(
|
||||
['submodule', 'status', '--'],
|
||||
repository.path,
|
||||
'listSubmodules'
|
||||
'listSubmodules',
|
||||
{
|
||||
successExitCodes: new Set([0, 128]),
|
||||
}
|
||||
)
|
||||
|
||||
if (result.exitCode === 128) {
|
||||
// unable to parse submodules in repository, giving up
|
||||
return []
|
||||
}
|
||||
|
||||
const submodules = new Array<SubmoduleEntry>()
|
||||
|
||||
for (const entry of result.stdout.split('\n')) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { getDotComAPIEndpoint } from './api'
|
|||
* @param email The email address associated with a user
|
||||
* @param size The size (in pixels) of the avatar to render
|
||||
*/
|
||||
export function generateGravatarUrl(email: string, size: number = 200): string {
|
||||
export function generateGravatarUrl(email: string, size: number = 60): string {
|
||||
const input = email.trim().toLowerCase()
|
||||
const hash = crypto
|
||||
.createHash('md5')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { spawn } from 'child_process'
|
||||
import { spawn, ChildProcess } from 'child_process'
|
||||
import { assertNever } from '../fatal-error'
|
||||
import { IFoundShell } from './found-shell'
|
||||
|
||||
|
@ -76,11 +76,11 @@ export async function getAvailableShells(): Promise<
|
|||
return shells
|
||||
}
|
||||
|
||||
export async function launch(
|
||||
export function launch(
|
||||
foundShell: IFoundShell<Shell>,
|
||||
path: string
|
||||
): Promise<void> {
|
||||
): ChildProcess {
|
||||
const bundleID = getBundleID(foundShell.shell)
|
||||
const commandArgs = ['-b', bundleID, path]
|
||||
await spawn('open', commandArgs)
|
||||
return spawn('open', commandArgs)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { spawn } from 'child_process'
|
||||
import { spawn, ChildProcess } from 'child_process'
|
||||
import { pathExists } from '../file-system'
|
||||
import { assertNever } from '../fatal-error'
|
||||
import { IFoundShell } from './found-shell'
|
||||
|
@ -99,26 +99,22 @@ export async function getAvailableShells(): Promise<
|
|||
return shells
|
||||
}
|
||||
|
||||
export async function launch(
|
||||
shell: IFoundShell<Shell>,
|
||||
export function launch(
|
||||
foundShell: IFoundShell<Shell>,
|
||||
path: string
|
||||
): Promise<void> {
|
||||
if (shell.shell === Shell.Urxvt) {
|
||||
const commandArgs = ['-cd', path]
|
||||
await spawn(shell.path, commandArgs)
|
||||
): ChildProcess {
|
||||
const shell = foundShell.shell
|
||||
switch (shell) {
|
||||
case Shell.Urxvt:
|
||||
return spawn(foundShell.path, ['-cd', path])
|
||||
case Shell.Konsole:
|
||||
return spawn(foundShell.path, ['--workdir', path])
|
||||
case Shell.Xterm:
|
||||
return spawn(foundShell.path, ['-e', '/bin/bash'], { cwd: path })
|
||||
case Shell.Tilix:
|
||||
case Shell.Gnome:
|
||||
return spawn(foundShell.path, ['--working-directory', path])
|
||||
default:
|
||||
return assertNever(shell, `Unknown shell: ${shell}`)
|
||||
}
|
||||
|
||||
if (shell.shell === Shell.Konsole) {
|
||||
const commandArgs = ['--workdir', path]
|
||||
await spawn(shell.path, commandArgs)
|
||||
}
|
||||
|
||||
if (shell.shell === Shell.Xterm) {
|
||||
const commandArgs = ['-e', '/bin/bash']
|
||||
const commandOptions = { cwd: path }
|
||||
await spawn(shell.path, commandArgs, commandOptions)
|
||||
}
|
||||
|
||||
const commandArgs = ['--working-directory', path]
|
||||
await spawn(shell.path, commandArgs)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { ChildProcess } from 'child_process'
|
||||
|
||||
import * as Darwin from './darwin'
|
||||
import * as Win32 from './win32'
|
||||
import * as Linux from './linux'
|
||||
|
@ -71,7 +73,11 @@ export async function findShellOrDefault(shell: Shell): Promise<FoundShell> {
|
|||
}
|
||||
|
||||
/** Launch the given shell at the path. */
|
||||
export async function launchShell(shell: FoundShell, path: string) {
|
||||
export async function launchShell(
|
||||
shell: FoundShell,
|
||||
path: string,
|
||||
onError: (error: Error) => void
|
||||
): Promise<void> {
|
||||
// We have to manually cast the wider `Shell` type into the platform-specific
|
||||
// type. This is less than ideal, but maybe the best we can do without
|
||||
// platform-specific build targets.
|
||||
|
@ -85,15 +91,46 @@ export async function launchShell(shell: FoundShell, path: string) {
|
|||
)
|
||||
}
|
||||
|
||||
let cp: ChildProcess | null = null
|
||||
|
||||
if (__DARWIN__) {
|
||||
return Darwin.launch(shell as IFoundShell<Darwin.Shell>, path)
|
||||
cp = Darwin.launch(shell as IFoundShell<Darwin.Shell>, path)
|
||||
} else if (__WIN32__) {
|
||||
return Win32.launch(shell as IFoundShell<Win32.Shell>, path)
|
||||
cp = Win32.launch(shell as IFoundShell<Win32.Shell>, path)
|
||||
} else if (__LINUX__) {
|
||||
return Linux.launch(shell as IFoundShell<Linux.Shell>, path)
|
||||
cp = Linux.launch(shell as IFoundShell<Linux.Shell>, path)
|
||||
}
|
||||
|
||||
return Promise.reject(
|
||||
`Platform not currently supported for launching shells: ${process.platform}`
|
||||
)
|
||||
if (cp != null) {
|
||||
addErrorTracing(shell.shell, cp, onError)
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
return Promise.reject(
|
||||
`Platform not currently supported for launching shells: ${
|
||||
process.platform
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function addErrorTracing(
|
||||
shell: Shell,
|
||||
cp: ChildProcess,
|
||||
onError: (error: Error) => void
|
||||
) {
|
||||
cp.stderr.on('data', chunk => {
|
||||
const text = chunk instanceof Buffer ? chunk.toString() : chunk
|
||||
log.debug(`[${shell}] stderr: '${text}'`)
|
||||
})
|
||||
|
||||
cp.on('error', err => {
|
||||
log.debug(`[${shell}] an error was encountered`, err)
|
||||
onError(err)
|
||||
})
|
||||
|
||||
cp.on('exit', code => {
|
||||
if (code !== 0) {
|
||||
log.debug(`[${shell}] exit code: ${code}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -114,52 +114,36 @@ export async function getAvailableShells(): Promise<
|
|||
return shells
|
||||
}
|
||||
|
||||
function addErrorTracing(context: string, cp: ChildProcess) {
|
||||
cp.stderr.on('data', chunk => {
|
||||
const text = chunk instanceof Buffer ? chunk.toString() : chunk
|
||||
log.debug(`[${context}] stderr: '${text}'`)
|
||||
})
|
||||
|
||||
cp.on('exit', code => {
|
||||
if (code !== 0) {
|
||||
log.debug(`[${context}] exit code: ${code}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function launch(
|
||||
export function launch(
|
||||
foundShell: IFoundShell<Shell>,
|
||||
path: string
|
||||
): Promise<void> {
|
||||
): ChildProcess {
|
||||
const shell = foundShell.shell
|
||||
|
||||
if (shell === Shell.PowerShell) {
|
||||
const psCommand = `"Set-Location -LiteralPath '${path}'"`
|
||||
const cp = spawn(
|
||||
'START',
|
||||
['powershell', '-NoExit', '-Command', psCommand],
|
||||
{
|
||||
switch (shell) {
|
||||
case Shell.PowerShell:
|
||||
const psCommand = `"Set-Location -LiteralPath '${path}'"`
|
||||
return spawn('START', ['powershell', '-NoExit', '-Command', psCommand], {
|
||||
shell: true,
|
||||
cwd: path,
|
||||
}
|
||||
)
|
||||
addErrorTracing(`PowerShell`, cp)
|
||||
} else if (shell === Shell.Hyper) {
|
||||
const cp = spawn(`"${foundShell.path}"`, [`"${path}"`], {
|
||||
shell: true,
|
||||
cwd: path,
|
||||
})
|
||||
addErrorTracing(`Hyper`, cp)
|
||||
} else if (shell === Shell.GitBash) {
|
||||
const cp = spawn(`"${foundShell.path}"`, [`--cd="${path}"`], {
|
||||
shell: true,
|
||||
cwd: path,
|
||||
})
|
||||
addErrorTracing(`Git Bash`, cp)
|
||||
} else if (shell === Shell.Cmd) {
|
||||
const cp = spawn('START', ['cmd'], { shell: true, cwd: path })
|
||||
addErrorTracing(`CMD`, cp)
|
||||
} else {
|
||||
assertNever(shell, `Unknown shell: ${shell}`)
|
||||
})
|
||||
case Shell.Hyper:
|
||||
const hyperPath = `"${foundShell.path}"`
|
||||
log.info(`launching ${shell} at path: ${hyperPath}`)
|
||||
return spawn(hyperPath, [`"${path}"`], {
|
||||
shell: true,
|
||||
cwd: path,
|
||||
})
|
||||
case Shell.GitBash:
|
||||
const gitBashPath = `"${foundShell.path}"`
|
||||
log.info(`launching ${shell} at path: ${gitBashPath}`)
|
||||
return spawn(gitBashPath, [`--cd="${path}"`], {
|
||||
shell: true,
|
||||
cwd: path,
|
||||
})
|
||||
case Shell.Cmd:
|
||||
return spawn('START', ['cmd'], { shell: true, cwd: path })
|
||||
default:
|
||||
return assertNever(shell, `Unknown shell: ${shell}`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -406,7 +406,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
defaultBranch: null,
|
||||
allBranches: new Array<Branch>(),
|
||||
recentBranches: new Array<Branch>(),
|
||||
openPullRequests: [],
|
||||
openPullRequests: new Array<PullRequest>(),
|
||||
currentPullRequest: null,
|
||||
isLoadingPullRequests: false,
|
||||
},
|
||||
|
@ -792,14 +792,15 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
const previouslySelectedRepository = this.selectedRepository
|
||||
|
||||
this.selectedRepository = repository
|
||||
this.emitUpdate()
|
||||
|
||||
this.emitUpdate()
|
||||
this.stopBackgroundFetching()
|
||||
this.stopPullRequestUpdater()
|
||||
|
||||
if (!repository) {
|
||||
if (repository == null) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
if (!(repository instanceof Repository)) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
@ -815,12 +816,37 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
const gitHubRepository = repository.gitHubRepository
|
||||
if (gitHubRepository) {
|
||||
this._updateIssues(gitHubRepository)
|
||||
}
|
||||
this._refreshRepository(repository)
|
||||
|
||||
await this._refreshRepository(repository)
|
||||
const gitHubRepository = repository.gitHubRepository
|
||||
|
||||
if (gitHubRepository != null) {
|
||||
this._updateIssues(gitHubRepository)
|
||||
this.loadPullRequests(repository, async () => {
|
||||
const promiseForPRs = this.pullRequestStore.fetchPullRequestsFromCache(
|
||||
gitHubRepository
|
||||
)
|
||||
const isLoading = this.pullRequestStore.isFetchingPullRequests(
|
||||
gitHubRepository
|
||||
)
|
||||
|
||||
const prs = await promiseForPRs
|
||||
|
||||
if (prs.length > 0) {
|
||||
this.updateBranchesState(repository, state => {
|
||||
return {
|
||||
openPullRequests: prs,
|
||||
isLoadingPullRequests: isLoading,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this._refreshPullRequests(repository)
|
||||
}
|
||||
|
||||
this._updateCurrentPullRequest(repository)
|
||||
this.emitUpdate()
|
||||
})
|
||||
}
|
||||
|
||||
// The selected repository could have changed while we were refreshing.
|
||||
if (this.selectedRepository !== repository) {
|
||||
|
@ -835,10 +861,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
this.startBackgroundFetching(repository, !previouslySelectedRepository)
|
||||
this.startPullRequestUpdater(repository)
|
||||
|
||||
this.refreshMentionables(repository)
|
||||
|
||||
this.addUpstreamRemoteIfNeeded(repository)
|
||||
this._refreshPullRequests(repository)
|
||||
|
||||
return this._repositoryWithRefreshedGitHubRepository(repository)
|
||||
}
|
||||
|
@ -2364,7 +2390,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
} finally {
|
||||
this.updatePushPullFetchProgress(repository, null)
|
||||
|
||||
if (fetchType !== FetchType.BackgroundTask) {
|
||||
if (fetchType === FetchType.UserInitiatedTask) {
|
||||
this._refreshPullRequests(repository)
|
||||
}
|
||||
}
|
||||
|
@ -2488,7 +2514,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
try {
|
||||
const match = await findShellOrDefault(this.selectedShell)
|
||||
await launchShell(match, path)
|
||||
await launchShell(match, path, error => this._pushError(error))
|
||||
} catch (error) {
|
||||
this.emitError(error)
|
||||
}
|
||||
|
@ -3027,7 +3053,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
await this._openInBrowser(baseURL)
|
||||
}
|
||||
|
||||
public async _refreshPullRequests(repository: Repository): Promise<void> {
|
||||
private async loadPullRequests(
|
||||
repository: Repository,
|
||||
loader: (account: Account) => void
|
||||
) {
|
||||
const gitHubRepository = repository.gitHubRepository
|
||||
|
||||
if (gitHubRepository == null) {
|
||||
|
@ -3043,12 +3072,18 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
return
|
||||
}
|
||||
|
||||
await this.pullRequestStore.refreshPullRequests(repository, account)
|
||||
this.updateMenuItemLabels(repository)
|
||||
await loader(account)
|
||||
}
|
||||
|
||||
public async _refreshPullRequests(repository: Repository): Promise<void> {
|
||||
return this.loadPullRequests(repository, async account => {
|
||||
await this.pullRequestStore.fetchAndCachePullRequests(repository, account)
|
||||
this.updateMenuItemLabels(repository)
|
||||
})
|
||||
}
|
||||
|
||||
private async onPullRequestStoreUpdated(gitHubRepository: GitHubRepository) {
|
||||
const pullRequests = await this.pullRequestStore.getPullRequests(
|
||||
const promiseForPRs = this.pullRequestStore.fetchPullRequestsFromCache(
|
||||
gitHubRepository
|
||||
)
|
||||
const isLoading = this.pullRequestStore.isFetchingPullRequests(
|
||||
|
@ -3064,15 +3099,15 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
return
|
||||
}
|
||||
|
||||
const prs = await promiseForPRs
|
||||
this.updateBranchesState(repository, state => {
|
||||
return {
|
||||
openPullRequests: pullRequests,
|
||||
openPullRequests: prs,
|
||||
isLoadingPullRequests: isLoading,
|
||||
}
|
||||
})
|
||||
|
||||
this._updateCurrentPullRequest(repository)
|
||||
|
||||
this.emitUpdate()
|
||||
}
|
||||
|
||||
|
@ -3083,21 +3118,20 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
remote: IRemote
|
||||
): PullRequest | null {
|
||||
const upstream = branch.upstreamWithoutRemote
|
||||
if (!upstream) {
|
||||
|
||||
if (upstream == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const pr of pullRequests) {
|
||||
if (
|
||||
pr.head.ref === upstream &&
|
||||
pr.head.gitHubRepository &&
|
||||
pr.head.gitHubRepository.cloneURL === remote.url
|
||||
) {
|
||||
return pr
|
||||
}
|
||||
}
|
||||
const pr =
|
||||
pullRequests.find(
|
||||
pr =>
|
||||
pr.head.ref === upstream &&
|
||||
pr.head.gitHubRepository != null &&
|
||||
pr.head.gitHubRepository.cloneURL === remote.url
|
||||
) || null
|
||||
|
||||
return null
|
||||
return pr
|
||||
}
|
||||
|
||||
private _updateCurrentPullRequest(repository: Repository) {
|
||||
|
@ -3228,9 +3262,16 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
const gitStore = this.getGitStore(repository)
|
||||
|
||||
await this.withAuthenticatingUser(repository, async (repo, account) => {
|
||||
await gitStore.fetchRemote(account, remoteName, false)
|
||||
await gitStore.fetchRemote(account, remoteName, false, progress => {
|
||||
this.updatePushPullFetchProgress(repository, {
|
||||
...progress,
|
||||
value: progress.value,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
this.updatePushPullFetchProgress(repository, null)
|
||||
|
||||
const localBranchName = `pr/${pullRequest.number}`
|
||||
const doesBranchExist =
|
||||
gitStore.allBranches.find(branch => branch.name === localBranchName) !=
|
||||
|
|
|
@ -62,14 +62,14 @@ export class PullRequestUpdater {
|
|||
TimeoutHandles.PullRequest,
|
||||
|
||||
window.setTimeout(() => {
|
||||
this.store.refreshPullRequests(this.repository, this.account)
|
||||
this.store.fetchAndCachePullRequests(this.repository, this.account)
|
||||
}, PullRequestInterval)
|
||||
)
|
||||
|
||||
this.timeoutHandles.set(
|
||||
TimeoutHandles.Status,
|
||||
window.setTimeout(() => {
|
||||
this.store.refreshPullRequestStatuses(githubRepo, this.account)
|
||||
this.store.fetchPullRequestStatuses(githubRepo, this.account)
|
||||
}, StatusInterval)
|
||||
)
|
||||
}
|
||||
|
@ -112,8 +112,8 @@ export class PullRequestUpdater {
|
|||
this.repository.gitHubRepository
|
||||
)
|
||||
|
||||
await this.store.refreshPullRequestStatuses(githubRepo, this.account)
|
||||
const prs = await this.store.getPullRequests(githubRepo)
|
||||
await this.store.fetchPullRequestStatuses(githubRepo, this.account)
|
||||
const prs = await this.store.fetchPullRequestsFromCache(githubRepo)
|
||||
|
||||
for (const pr of prs) {
|
||||
const status = pr.status
|
||||
|
|
|
@ -25,12 +25,14 @@ import { IRemote } from '../../models/remote'
|
|||
*/
|
||||
export const ForkedRemotePrefix = 'github-desktop-'
|
||||
|
||||
const Decrement = (n: number) => n - 1
|
||||
const Increment = (n: number) => n + 1
|
||||
|
||||
/** The store for GitHub Pull Requests. */
|
||||
export class PullRequestStore extends TypedBaseStore<GitHubRepository> {
|
||||
private readonly pullRequestDatabase: PullRequestDatabase
|
||||
private readonly repositoriesStore: RepositoriesStore
|
||||
|
||||
private activeFetchCountPerRepository = new Map<number, number>()
|
||||
private readonly repositoryStore: RepositoriesStore
|
||||
private readonly activeFetchCountPerRepository = new Map<number, number>()
|
||||
|
||||
public constructor(
|
||||
db: PullRequestDatabase,
|
||||
|
@ -39,11 +41,11 @@ export class PullRequestStore extends TypedBaseStore<GitHubRepository> {
|
|||
super()
|
||||
|
||||
this.pullRequestDatabase = db
|
||||
this.repositoriesStore = repositoriesStore
|
||||
this.repositoryStore = repositoriesStore
|
||||
}
|
||||
|
||||
/** Loads all pull requests against the given repository. */
|
||||
public async refreshPullRequests(
|
||||
public async fetchAndCachePullRequests(
|
||||
repository: Repository,
|
||||
account: Account
|
||||
): Promise<void> {
|
||||
|
@ -51,45 +53,159 @@ export class PullRequestStore extends TypedBaseStore<GitHubRepository> {
|
|||
'Can only refresh pull requests for GitHub repositories',
|
||||
repository.gitHubRepository
|
||||
)
|
||||
const api = API.fromAccount(account)
|
||||
const apiClient = API.fromAccount(account)
|
||||
|
||||
this.changeActiveFetchCount(githubRepo, c => c + 1)
|
||||
this.updateActiveFetchCount(githubRepo, Increment)
|
||||
|
||||
try {
|
||||
const raw = await api.fetchPullRequests(
|
||||
const apiResult = await apiClient.fetchPullRequests(
|
||||
githubRepo.owner.login,
|
||||
githubRepo.name,
|
||||
'open'
|
||||
)
|
||||
|
||||
await this.writePRs(raw, githubRepo)
|
||||
await this.cachePullRequests(apiResult, githubRepo)
|
||||
|
||||
const prs = await this.getPullRequests(githubRepo)
|
||||
const prs = await this.fetchPullRequestsFromCache(githubRepo)
|
||||
|
||||
await this.refreshStatusForPRs(prs, githubRepo, account)
|
||||
await this.fetchAndCachePullRequestStatus(prs, githubRepo, account)
|
||||
await this.pruneForkedRemotes(repository, prs)
|
||||
|
||||
this.emitUpdate(githubRepo)
|
||||
} catch (error) {
|
||||
log.warn(`Error refreshing pull requests for '${repository.name}'`, error)
|
||||
this.emitError(error)
|
||||
} finally {
|
||||
this.changeActiveFetchCount(githubRepo, c => c - 1)
|
||||
this.updateActiveFetchCount(githubRepo, Decrement)
|
||||
}
|
||||
}
|
||||
|
||||
/** Is the store currently fetching the list of open pull requests? */
|
||||
public isFetchingPullRequests(repository: GitHubRepository): boolean {
|
||||
const repoDbId = forceUnwrap(
|
||||
'Cannot fetch PRs for a repository which is not in the database',
|
||||
repository.dbID
|
||||
)
|
||||
const currentCount = this.activeFetchCountPerRepository.get(repoDbId) || 0
|
||||
|
||||
return currentCount > 0
|
||||
}
|
||||
|
||||
/** Loads the status for the given pull request. */
|
||||
public async fetchPullRequestStatus(
|
||||
repository: GitHubRepository,
|
||||
account: Account,
|
||||
pullRequest: PullRequest
|
||||
): Promise<void> {
|
||||
await this.fetchAndCachePullRequestStatus(
|
||||
[pullRequest],
|
||||
repository,
|
||||
account
|
||||
)
|
||||
}
|
||||
|
||||
/** Loads the status for all pull request against a given repository. */
|
||||
public async fetchPullRequestStatuses(
|
||||
repository: GitHubRepository,
|
||||
account: Account
|
||||
): Promise<void> {
|
||||
const prs = await this.fetchPullRequestsFromCache(repository)
|
||||
|
||||
await this.fetchAndCachePullRequestStatus(prs, repository, account)
|
||||
}
|
||||
|
||||
/** Gets the pull requests against the given repository. */
|
||||
public async fetchPullRequestsFromCache(
|
||||
repository: GitHubRepository
|
||||
): Promise<ReadonlyArray<PullRequest>> {
|
||||
const gitHubRepositoryID = repository.dbID
|
||||
|
||||
if (gitHubRepositoryID == null) {
|
||||
return fatalError(
|
||||
"Cannot get pull requests for a repository that hasn't been inserted into the database!"
|
||||
)
|
||||
}
|
||||
|
||||
const records = await this.pullRequestDatabase.pullRequests
|
||||
.where('base.repoId')
|
||||
.equals(gitHubRepositoryID)
|
||||
.reverse()
|
||||
.sortBy('number')
|
||||
|
||||
const result = new Array<PullRequest>()
|
||||
|
||||
for (const record of records) {
|
||||
const repositoryDbId = record.head.repoId
|
||||
let githubRepository: GitHubRepository | null = null
|
||||
|
||||
if (repositoryDbId != null) {
|
||||
githubRepository = await this.repositoryStore.findGitHubRepositoryByID(
|
||||
repositoryDbId
|
||||
)
|
||||
}
|
||||
|
||||
// We know the base repo ID can't be null since it's the repository we
|
||||
// fetched the PR from in the first place.
|
||||
const parentRepositoryDbId = forceUnwrap(
|
||||
'A pull request cannot have a null base repo id',
|
||||
record.base.repoId
|
||||
)
|
||||
const parentGitGubRepository: GitHubRepository | null = await this.repositoryStore.findGitHubRepositoryByID(
|
||||
parentRepositoryDbId
|
||||
)
|
||||
const parentGitHubRepository = forceUnwrap(
|
||||
'PR cannot have a null base repo',
|
||||
parentGitGubRepository
|
||||
)
|
||||
|
||||
// We can be certain the PR ID is valid since we just got it from the
|
||||
// database.
|
||||
const pullRequestDbId = forceUnwrap(
|
||||
'PR cannot have a null ID after being retrieved from the database',
|
||||
record.id
|
||||
)
|
||||
|
||||
const pullRequestStatus = await this.findPullRequestStatus(
|
||||
record.head.sha,
|
||||
pullRequestDbId
|
||||
)
|
||||
|
||||
result.push(
|
||||
new PullRequest(
|
||||
pullRequestDbId,
|
||||
new Date(record.createdAt),
|
||||
pullRequestStatus,
|
||||
record.title,
|
||||
record.number,
|
||||
new PullRequestRef(
|
||||
record.head.ref,
|
||||
record.head.sha,
|
||||
githubRepository
|
||||
),
|
||||
new PullRequestRef(
|
||||
record.base.ref,
|
||||
record.base.sha,
|
||||
parentGitHubRepository
|
||||
),
|
||||
record.author
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private async pruneForkedRemotes(
|
||||
repository: Repository,
|
||||
pullRequests: ReadonlyArray<PullRequest>
|
||||
) {
|
||||
const remotes = await getRemotes(repository)
|
||||
const forkedRemotesToDelete = this.forkedRemotesToDelete(
|
||||
remotes,
|
||||
pullRequests
|
||||
)
|
||||
const forkedRemotesToDelete = this.getRemotesToDelete(remotes, pullRequests)
|
||||
|
||||
await this.deleteForkedRemotes(repository, forkedRemotesToDelete)
|
||||
await this.deleteRemotes(repository, forkedRemotesToDelete)
|
||||
}
|
||||
|
||||
private forkedRemotesToDelete(
|
||||
private getRemotesToDelete(
|
||||
remotes: ReadonlyArray<IRemote>,
|
||||
openPullRequests: ReadonlyArray<PullRequest>
|
||||
): ReadonlyArray<IRemote> {
|
||||
|
@ -97,131 +213,76 @@ export class PullRequestStore extends TypedBaseStore<GitHubRepository> {
|
|||
remote.name.startsWith(ForkedRemotePrefix)
|
||||
)
|
||||
const remotesOfPullRequests = new Set<string>()
|
||||
openPullRequests.forEach(openPullRequest => {
|
||||
const { gitHubRepository } = openPullRequest.head
|
||||
|
||||
openPullRequests.forEach(pr => {
|
||||
const { gitHubRepository } = pr.head
|
||||
|
||||
if (gitHubRepository != null && gitHubRepository.cloneURL != null) {
|
||||
remotesOfPullRequests.add(gitHubRepository.cloneURL)
|
||||
}
|
||||
})
|
||||
const forkedRemotesToDelete = forkedRemotes.filter(
|
||||
|
||||
const result = forkedRemotes.filter(
|
||||
forkedRemote => !remotesOfPullRequests.has(forkedRemote.url)
|
||||
)
|
||||
|
||||
return forkedRemotesToDelete
|
||||
return result
|
||||
}
|
||||
|
||||
private async deleteForkedRemotes(
|
||||
private async deleteRemotes(
|
||||
repository: Repository,
|
||||
remotes: ReadonlyArray<IRemote>
|
||||
) {
|
||||
for (const remote of remotes) {
|
||||
await removeRemote(repository, remote.name)
|
||||
}
|
||||
const promises: Array<Promise<void>> = []
|
||||
|
||||
remotes.forEach(r => promises.push(removeRemote(repository, r.name)))
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
private changeActiveFetchCount(
|
||||
private updateActiveFetchCount(
|
||||
repository: GitHubRepository,
|
||||
fn: (count: number) => number
|
||||
update: (count: number) => number
|
||||
) {
|
||||
const key = forceUnwrap(
|
||||
const repoDbId = forceUnwrap(
|
||||
'Cannot fetch PRs for a repository which is not in the database',
|
||||
repository.dbID
|
||||
)
|
||||
const currentCount = this.activeFetchCountPerRepository.get(key) || 0
|
||||
const newCount = fn(currentCount)
|
||||
this.activeFetchCountPerRepository.set(key, newCount)
|
||||
const currentCount = this.activeFetchCountPerRepository.get(repoDbId) || 0
|
||||
const newCount = update(currentCount)
|
||||
|
||||
this.activeFetchCountPerRepository.set(repoDbId, newCount)
|
||||
this.emitUpdate(repository)
|
||||
}
|
||||
|
||||
/** Is the store currently fetching the list of open pull requests? */
|
||||
public isFetchingPullRequests(repository: GitHubRepository): boolean {
|
||||
const key = forceUnwrap(
|
||||
'Cannot fetch PRs for a repository which is not in the database',
|
||||
repository.dbID
|
||||
)
|
||||
|
||||
const currentCount = this.activeFetchCountPerRepository.get(key) || 0
|
||||
return currentCount > 0
|
||||
}
|
||||
|
||||
/** Loads the status for the given pull request. */
|
||||
public async refreshSinglePullRequestStatus(
|
||||
repository: GitHubRepository,
|
||||
account: Account,
|
||||
pullRequest: PullRequest
|
||||
): Promise<void> {
|
||||
await this.refreshStatusForPRs([pullRequest], repository, account)
|
||||
}
|
||||
|
||||
/** Loads the status for all pull request against a given repository. */
|
||||
public async refreshPullRequestStatuses(
|
||||
repository: GitHubRepository,
|
||||
account: Account
|
||||
): Promise<void> {
|
||||
const prs = await this.getPullRequests(repository)
|
||||
|
||||
await this.refreshStatusForPRs(prs, repository, account)
|
||||
}
|
||||
|
||||
private async refreshStatusForPRs(
|
||||
private async fetchAndCachePullRequestStatus(
|
||||
pullRequests: ReadonlyArray<PullRequest>,
|
||||
repository: GitHubRepository,
|
||||
account: Account
|
||||
): Promise<void> {
|
||||
const api = API.fromAccount(account)
|
||||
const apiClient = API.fromAccount(account)
|
||||
const statuses: Array<IPullRequestStatus> = []
|
||||
const prs: Array<PullRequest> = []
|
||||
|
||||
for (const pr of pullRequests) {
|
||||
const apiStatus = await api.fetchCombinedRefStatus(
|
||||
const combinedRefStatus = await apiClient.fetchCombinedRefStatus(
|
||||
repository.owner.login,
|
||||
repository.name,
|
||||
pr.head.sha
|
||||
)
|
||||
|
||||
const combinedRefStatuses = apiStatus.statuses.map(x => {
|
||||
return {
|
||||
id: x.id,
|
||||
state: x.state,
|
||||
}
|
||||
})
|
||||
|
||||
const status = new PullRequestStatus(
|
||||
pr.number,
|
||||
apiStatus.state,
|
||||
apiStatus.total_count,
|
||||
pr.head.sha,
|
||||
combinedRefStatuses
|
||||
)
|
||||
|
||||
statuses.push({
|
||||
pullRequestId: pr.id,
|
||||
state: apiStatus.state,
|
||||
totalCount: apiStatus.total_count,
|
||||
state: combinedRefStatus.state,
|
||||
totalCount: combinedRefStatus.total_count,
|
||||
sha: pr.head.sha,
|
||||
statuses: apiStatus.statuses,
|
||||
statuses: combinedRefStatus.statuses,
|
||||
})
|
||||
|
||||
prs.push(
|
||||
new PullRequest(
|
||||
pr.id,
|
||||
pr.created,
|
||||
status,
|
||||
pr.title,
|
||||
pr.number,
|
||||
pr.head,
|
||||
pr.base,
|
||||
pr.author
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
await this.writePRStatus(statuses)
|
||||
await this.cachePullRequestStatuses(statuses)
|
||||
this.emitUpdate(repository)
|
||||
}
|
||||
|
||||
private async getPRStatusById(
|
||||
private async findPullRequestStatus(
|
||||
sha: string,
|
||||
pullRequestId: number
|
||||
): Promise<PullRequestStatus | null> {
|
||||
|
@ -251,146 +312,115 @@ export class PullRequestStore extends TypedBaseStore<GitHubRepository> {
|
|||
)
|
||||
}
|
||||
|
||||
private async writePRs(
|
||||
pullRequests: ReadonlyArray<IAPIPullRequest>,
|
||||
private async cachePullRequests(
|
||||
pullRequestsFromAPI: ReadonlyArray<IAPIPullRequest>,
|
||||
repository: GitHubRepository
|
||||
): Promise<void> {
|
||||
const repoId = repository.dbID
|
||||
const repoDbId = repository.dbID
|
||||
|
||||
if (!repoId) {
|
||||
fatalError(
|
||||
if (repoDbId == null) {
|
||||
return fatalError(
|
||||
"Cannot store pull requests for a repository that hasn't been inserted into the database!"
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const table = this.pullRequestDatabase.pullRequests
|
||||
const prsToInsert = new Array<IPullRequest>()
|
||||
let githubRepo: GitHubRepository | null = null
|
||||
|
||||
const insertablePRs = new Array<IPullRequest>()
|
||||
for (const pr of pullRequests) {
|
||||
let headRepo: GitHubRepository | null = null
|
||||
if (pr.head.repo) {
|
||||
headRepo = await this.repositoriesStore.findOrPutGitHubRepository(
|
||||
for (const pr of pullRequestsFromAPI) {
|
||||
// Once the repo is found on first try, no need to keep looking
|
||||
if (githubRepo == null && pr.head.repo != null) {
|
||||
githubRepo = await this.repositoryStore.upsertGitHubRepository(
|
||||
repository.endpoint,
|
||||
pr.head.repo
|
||||
)
|
||||
}
|
||||
|
||||
// We know the base repo isn't null since that's where we got the PR from
|
||||
// in the first place.
|
||||
const baseRepo = await this.repositoriesStore.findOrPutGitHubRepository(
|
||||
repository.endpoint,
|
||||
forceUnwrap('PR cannot have a null base repo', pr.base.repo)
|
||||
if (githubRepo == null) {
|
||||
return fatalError(
|
||||
"The PR doesn't seem to be associated with a GitHub repository"
|
||||
)
|
||||
}
|
||||
|
||||
const githubRepoDbId = forceUnwrap(
|
||||
'PR cannot have non-existent repo',
|
||||
githubRepo.dbID
|
||||
)
|
||||
|
||||
insertablePRs.push({
|
||||
// We know the base repo isn't null since that's where we got the PR from
|
||||
// in the first place.
|
||||
const parentRepo = forceUnwrap(
|
||||
'PR cannot have a null base repo',
|
||||
pr.base.repo
|
||||
)
|
||||
const parentGitHubRepo = await this.repositoryStore.upsertGitHubRepository(
|
||||
repository.endpoint,
|
||||
parentRepo
|
||||
)
|
||||
const parentGitHubRepoDbId = forceUnwrap(
|
||||
'PR cannot have a null parent database id',
|
||||
parentGitHubRepo.dbID
|
||||
)
|
||||
|
||||
prsToInsert.push({
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
createdAt: pr.created_at,
|
||||
head: {
|
||||
ref: pr.head.ref,
|
||||
sha: pr.head.sha,
|
||||
repoId: headRepo ? headRepo.dbID! : null,
|
||||
repoId: githubRepoDbId,
|
||||
},
|
||||
base: {
|
||||
ref: pr.base.ref,
|
||||
sha: pr.base.sha,
|
||||
repoId: forceUnwrap('PR cannot have a null base repo', baseRepo.dbID),
|
||||
repoId: parentGitHubRepoDbId,
|
||||
},
|
||||
author: pr.user.login,
|
||||
})
|
||||
}
|
||||
|
||||
await this.pullRequestDatabase.transaction('rw', table, async () => {
|
||||
await table.clear()
|
||||
return await table.bulkAdd(insertablePRs)
|
||||
if (prsToInsert.length <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.pullRequestDatabase.transaction('rw', table, async () => {
|
||||
// since all PRs come from the same repository
|
||||
// using the base repoId of the fist element
|
||||
// is sufficient here
|
||||
const repoDbId = prsToInsert[0].base.repoId!
|
||||
|
||||
// we need to delete the stales PRs from the db
|
||||
// so we remove all for a repo to avoid having to
|
||||
// do diffing
|
||||
await table
|
||||
.where('base.repoId')
|
||||
.equals(repoDbId)
|
||||
.delete()
|
||||
|
||||
await table.bulkAdd(prsToInsert)
|
||||
})
|
||||
}
|
||||
|
||||
private async writePRStatus(
|
||||
private async cachePullRequestStatuses(
|
||||
statuses: Array<IPullRequestStatus>
|
||||
): Promise<void> {
|
||||
const table = this.pullRequestDatabase.pullRequestStatus
|
||||
|
||||
await this.pullRequestDatabase.transaction('rw', table, async () => {
|
||||
for (const status of statuses) {
|
||||
const existing = await table
|
||||
const record = await table
|
||||
.where('[sha+pullRequestId]')
|
||||
.equals([status.sha, status.pullRequestId])
|
||||
.first()
|
||||
if (existing) {
|
||||
await table.put({ id: existing.id, ...status })
|
||||
} else {
|
||||
|
||||
if (record == null) {
|
||||
await table.add(status)
|
||||
} else {
|
||||
await table.put({ id: record.id, ...status })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Gets the pull requests against the given repository. */
|
||||
public async getPullRequests(
|
||||
repository: GitHubRepository
|
||||
): Promise<ReadonlyArray<PullRequest>> {
|
||||
const gitHubRepositoryID = repository.dbID
|
||||
|
||||
if (!gitHubRepositoryID) {
|
||||
return fatalError(
|
||||
"Cannot get pull requests for a repository that hasn't been inserted into the database!"
|
||||
)
|
||||
}
|
||||
|
||||
const raw = await this.pullRequestDatabase.pullRequests
|
||||
.where('base.repoId')
|
||||
.equals(gitHubRepositoryID)
|
||||
.reverse()
|
||||
.sortBy('number')
|
||||
|
||||
const pullRequests = new Array<PullRequest>()
|
||||
|
||||
for (const pr of raw) {
|
||||
const headId = pr.head.repoId
|
||||
|
||||
let head: GitHubRepository | null = null
|
||||
|
||||
if (headId) {
|
||||
head = await this.repositoriesStore.findGitHubRepositoryByID(headId)
|
||||
}
|
||||
|
||||
// We know the base repo ID can't be null since it's the repository we
|
||||
// fetched the PR from in the first place.
|
||||
const baseId = forceUnwrap(
|
||||
'PR cannot have a null base repo id',
|
||||
pr.base.repoId
|
||||
)
|
||||
const base = forceUnwrap(
|
||||
'PR cannot have a null base repo',
|
||||
await this.repositoriesStore.findGitHubRepositoryByID(baseId)
|
||||
)
|
||||
|
||||
// We can be certain the PR ID is valid since we just got it from the
|
||||
// database.
|
||||
const prID = forceUnwrap(
|
||||
'PR cannot have a null ID after being retrieved from the database',
|
||||
pr.id
|
||||
)
|
||||
|
||||
const pullRequestStatus = await this.getPRStatusById(pr.head.sha, prID)
|
||||
|
||||
const pullRequest = new PullRequest(
|
||||
prID,
|
||||
new Date(pr.createdAt),
|
||||
pullRequestStatus,
|
||||
pr.title,
|
||||
pr.number,
|
||||
new PullRequestRef(pr.head.ref, pr.head.sha, head),
|
||||
new PullRequestRef(pr.base.ref, pr.base.sha, base),
|
||||
pr.author
|
||||
)
|
||||
|
||||
pullRequests.push(pullRequest)
|
||||
}
|
||||
|
||||
return pullRequests
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export class RepositoriesStore extends BaseStore {
|
|||
}
|
||||
|
||||
/** Find the matching GitHub repository or add it if it doesn't exist. */
|
||||
public async findOrPutGitHubRepository(
|
||||
public async upsertGitHubRepository(
|
||||
endpoint: string,
|
||||
apiRepository: IAPIRepository
|
||||
): Promise<GitHubRepository> {
|
||||
|
@ -36,7 +36,8 @@ export class RepositoriesStore extends BaseStore {
|
|||
.equals(apiRepository.clone_url)
|
||||
.limit(1)
|
||||
.first()
|
||||
if (!gitHubRepository) {
|
||||
|
||||
if (gitHubRepository == null) {
|
||||
return this.putGitHubRepository(endpoint, apiRepository)
|
||||
} else {
|
||||
return this.buildGitHubRepository(gitHubRepository)
|
||||
|
@ -49,7 +50,8 @@ export class RepositoriesStore extends BaseStore {
|
|||
dbRepo: IDatabaseGitHubRepository
|
||||
): Promise<GitHubRepository> {
|
||||
const owner = await this.db.owners.get(dbRepo.ownerID)
|
||||
if (!owner) {
|
||||
|
||||
if (owner == null) {
|
||||
throw new Error(`Couldn't find the owner for ${dbRepo.name}`)
|
||||
}
|
||||
|
||||
|
@ -128,26 +130,27 @@ export class RepositoriesStore extends BaseStore {
|
|||
this.db.owners,
|
||||
async () => {
|
||||
const repos = await this.db.repositories.toArray()
|
||||
const existing = repos.find(r => r.path === path)
|
||||
let id: number
|
||||
const record = repos.find(r => r.path === path)
|
||||
let recordId: number
|
||||
let gitHubRepo: GitHubRepository | null = null
|
||||
if (existing) {
|
||||
id = existing.id!
|
||||
|
||||
if (existing.gitHubRepositoryID) {
|
||||
if (record != null) {
|
||||
recordId = record.id!
|
||||
|
||||
if (record.gitHubRepositoryID != null) {
|
||||
gitHubRepo = await this.findGitHubRepositoryByID(
|
||||
existing.gitHubRepositoryID
|
||||
record.gitHubRepositoryID
|
||||
)
|
||||
}
|
||||
} else {
|
||||
id = await this.db.repositories.add({
|
||||
recordId = await this.db.repositories.add({
|
||||
path,
|
||||
gitHubRepositoryID: null,
|
||||
missing: false,
|
||||
})
|
||||
}
|
||||
|
||||
return new Repository(path, id, gitHubRepo, false)
|
||||
return new Repository(path, recordId, gitHubRepo, false)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -110,11 +110,16 @@ async function formatGitIgnoreContents(
|
|||
return
|
||||
}
|
||||
|
||||
const linesEndInCRLF = autocrlf === 'true'
|
||||
if (linesEndInCRLF) {
|
||||
if (autocrlf == null) {
|
||||
// fallback to Git default behaviour
|
||||
resolve(`${text}\n`)
|
||||
} else {
|
||||
resolve(`${text}\r\n`)
|
||||
const linesEndInCRLF = autocrlf === 'true'
|
||||
if (linesEndInCRLF) {
|
||||
resolve(`${text}\n`)
|
||||
} else {
|
||||
resolve(`${text}\r\n`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ export class AppWindow {
|
|||
// Enable, among other things, the ResizeObserver
|
||||
experimentalFeatures: true,
|
||||
},
|
||||
acceptFirstMouse: true,
|
||||
}
|
||||
|
||||
if (__DARWIN__) {
|
||||
|
|
|
@ -28,7 +28,7 @@ function getFallbackAvatarUrlForAuthor(
|
|||
) {
|
||||
return `https://avatars.githubusercontent.com/u/e?email=${encodeURIComponent(
|
||||
author.email
|
||||
)}&s=40`
|
||||
)}&s=60`
|
||||
}
|
||||
|
||||
return generateGravatarUrl(author.email)
|
||||
|
|
|
@ -1512,7 +1512,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
private renderBranchToolbarButton(): JSX.Element | null {
|
||||
const selection = this.state.selectedState
|
||||
|
||||
if (!selection || selection.type !== SelectionType.Repository) {
|
||||
if (selection == null || selection.type !== SelectionType.Repository) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import { GitHubUserStore } from '../../lib/stores'
|
|||
import { GitHubRepository } from '../../models/github-repository'
|
||||
import { Account } from '../../models/account'
|
||||
import { IGitHubUser } from '../../lib/databases/index'
|
||||
import { validLoginExpression } from '../../lib/api'
|
||||
|
||||
/** An autocompletion hit for a user. */
|
||||
export interface IUserHit {
|
||||
|
@ -105,15 +104,6 @@ export class UserAutocompletionProvider
|
|||
return null
|
||||
}
|
||||
|
||||
// Since we might be looking up stuff in the API it's
|
||||
// important we sanitize this input or someone could lead with
|
||||
// ../ and then start GETing random resources in the API.
|
||||
// Not that they should be able to do any harm with just GET
|
||||
// but still, it ain't cool
|
||||
if (!validLoginExpression.test(login)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const user = await this.gitHubUserStore.getByLogin(this.account, login)
|
||||
|
||||
if (!user) {
|
||||
|
|
|
@ -143,7 +143,10 @@ export class BranchList extends React.Component<
|
|||
this.state = createState(props)
|
||||
}
|
||||
|
||||
private renderItem = (item: IBranchListItem) => {
|
||||
private renderItem = (
|
||||
item: IBranchListItem,
|
||||
matches: ReadonlyArray<number>
|
||||
) => {
|
||||
const branch = item.branch
|
||||
const commit = branch.tip
|
||||
const currentBranchName = this.props.currentBranch
|
||||
|
@ -154,7 +157,7 @@ export class BranchList extends React.Component<
|
|||
name={branch.name}
|
||||
isCurrentBranch={branch.name === currentBranchName}
|
||||
lastCommitDate={commit ? commit.author.date : null}
|
||||
filterText={this.props.filterText}
|
||||
matches={matches}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from 'react'
|
|||
import * as moment from 'moment'
|
||||
|
||||
import { Octicon, OcticonSymbol } from '../octicons'
|
||||
import { HighlightText } from '../lib/highlight-text'
|
||||
|
||||
interface IBranchProps {
|
||||
readonly name: string
|
||||
|
@ -10,34 +11,12 @@ interface IBranchProps {
|
|||
/** The date may be null if we haven't loaded the tip commit yet. */
|
||||
readonly lastCommitDate: Date | null
|
||||
|
||||
/** The current filter text to render */
|
||||
readonly filterText: string
|
||||
/** The characters in the branch name to highlight */
|
||||
readonly matches: ReadonlyArray<number>
|
||||
}
|
||||
|
||||
/** The branch component. */
|
||||
export class BranchListItem extends React.Component<IBranchProps, {}> {
|
||||
private renderHighlightedName(name: string) {
|
||||
const filterText = this.props.filterText
|
||||
const matchStart = name.indexOf(filterText)
|
||||
const matchLength = filterText.length
|
||||
|
||||
if (matchStart === -1) {
|
||||
return (
|
||||
<div className="name" title={name}>
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="name" title={name}>
|
||||
{name.substr(0, matchStart)}
|
||||
<mark>{name.substr(matchStart, matchLength)}</mark>
|
||||
{name.substr(matchStart + matchLength)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const lastCommitDate = this.props.lastCommitDate
|
||||
const isCurrentBranch = this.props.isCurrentBranch
|
||||
|
@ -51,7 +30,9 @@ export class BranchListItem extends React.Component<IBranchProps, {}> {
|
|||
return (
|
||||
<div className="branches-list-item">
|
||||
<Octicon className="icon" symbol={icon} />
|
||||
{this.renderHighlightedName(name)}
|
||||
<div className="name" title={name}>
|
||||
<HighlightText text={name} highlight={this.props.matches} />
|
||||
</div>
|
||||
<div className="description" title={infoTitle}>
|
||||
{date}
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as classNames from 'classnames'
|
|||
import { Octicon, OcticonSymbol } from '../octicons'
|
||||
import { CIStatus } from './ci-status'
|
||||
import { PullRequestStatus } from '../../models/pull-request'
|
||||
import { HighlightText } from '../lib/highlight-text'
|
||||
|
||||
export interface IPullRequestListItemProps {
|
||||
/** The title. */
|
||||
|
@ -29,6 +30,9 @@ export interface IPullRequestListItemProps {
|
|||
* inside the list item.
|
||||
*/
|
||||
readonly loading?: boolean
|
||||
|
||||
/** The characters in the PR title to highlight */
|
||||
readonly matches: ReadonlyArray<number>
|
||||
}
|
||||
|
||||
/** Pull requests as rendered in the Pull Requests list. */
|
||||
|
@ -57,7 +61,7 @@ export class PullRequestListItem extends React.Component<
|
|||
<Octicon className="icon" symbol={OcticonSymbol.gitPullRequest} />
|
||||
<div className="info">
|
||||
<div className="title" title={title}>
|
||||
{title}
|
||||
<HighlightText text={title || ''} highlight={this.props.matches} />
|
||||
</div>
|
||||
<div className="subtitle" title={subtitle}>
|
||||
{subtitle}
|
||||
|
|
|
@ -115,7 +115,10 @@ export class PullRequestList extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private renderPullRequest = (item: IPullRequestListItem) => {
|
||||
private renderPullRequest = (
|
||||
item: IPullRequestListItem,
|
||||
matches: ReadonlyArray<number>
|
||||
) => {
|
||||
const pr = item.pullRequest
|
||||
const refStatuses = pr.status != null ? pr.status.statuses : []
|
||||
const status =
|
||||
|
@ -136,6 +139,7 @@ export class PullRequestList extends React.Component<
|
|||
created={pr.created}
|
||||
author={pr.author}
|
||||
status={status}
|
||||
matches={matches}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ const prLoadingItemProps: IPullRequestListItemProps = {
|
|||
created: new Date(0),
|
||||
number: 0,
|
||||
title: '',
|
||||
matches: [],
|
||||
status: {
|
||||
sha: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
|
||||
totalCount: 1,
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
groupRepositories,
|
||||
YourRepositoriesIdentifier,
|
||||
} from './group-repositories'
|
||||
import { HighlightText } from '../lib/highlight-text'
|
||||
|
||||
interface ICloneGithubRepositoryProps {
|
||||
/** The account to clone from. */
|
||||
|
@ -36,6 +37,9 @@ interface ICloneGithubRepositoryProps {
|
|||
|
||||
/** Called when a repository is selected. */
|
||||
readonly onGitHubRepositorySelected: (url: string) => void
|
||||
|
||||
/** Should the component clear the filter text on render? */
|
||||
readonly shouldClearFilter: boolean
|
||||
}
|
||||
|
||||
interface ICloneGithubRepositoryState {
|
||||
|
@ -114,6 +118,12 @@ export class CloneGithubRepository extends React.Component<
|
|||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: ICloneGithubRepositoryProps) {
|
||||
if (nextProps.shouldClearFilter) {
|
||||
this.setState({
|
||||
filterText: '',
|
||||
})
|
||||
}
|
||||
|
||||
if (nextProps.account.id !== this.props.account.id) {
|
||||
this.loadRepositories(nextProps.account)
|
||||
}
|
||||
|
@ -205,12 +215,15 @@ export class CloneGithubRepository extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private renderItem = (item: IClonableRepositoryListItem) => {
|
||||
private renderItem = (
|
||||
item: IClonableRepositoryListItem,
|
||||
matches: ReadonlyArray<number>
|
||||
) => {
|
||||
return (
|
||||
<div className="clone-repository-list-item">
|
||||
<Octicon className="icon" symbol={item.icon} />
|
||||
<div className="name" title={name}>
|
||||
{item.text}
|
||||
<div className="name" title={item.text}>
|
||||
<HighlightText text={item.text} highlight={matches} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -61,6 +61,9 @@ interface ICloneRepositoryState {
|
|||
* The repository identifier that was last parsed from the user-entered URL.
|
||||
*/
|
||||
readonly lastParsedIdentifier: IRepositoryIdentifier | null
|
||||
|
||||
/** Should the component clear the filter text on render? */
|
||||
readonly shouldClearFilter: boolean
|
||||
}
|
||||
|
||||
/** The component for cloning a repository. */
|
||||
|
@ -77,9 +80,16 @@ export class CloneRepository extends React.Component<
|
|||
loading: false,
|
||||
error: null,
|
||||
lastParsedIdentifier: null,
|
||||
shouldClearFilter: false,
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: ICloneRepositoryProps) {
|
||||
this.setState({
|
||||
shouldClearFilter: this.props.selectedTab !== nextProps.selectedTab,
|
||||
})
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const initialURL = this.props.initialURL
|
||||
if (initialURL) {
|
||||
|
@ -176,6 +186,7 @@ export class CloneRepository extends React.Component<
|
|||
onGitHubRepositorySelected={this.updateUrl}
|
||||
onChooseDirectory={this.onChooseDirectory}
|
||||
onDismissed={this.props.onDismissed}
|
||||
shouldClearFilter={this.state.shouldClearFilter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { CommitListItem } from './commit-list-item'
|
|||
import { List } from '../lib/list'
|
||||
import { IGitHubUser } from '../../lib/databases'
|
||||
|
||||
const RowHeight = 48
|
||||
const RowHeight = 50
|
||||
|
||||
interface ICommitListProps {
|
||||
readonly onCommitChanged: (commit: Commit) => void
|
||||
|
|
|
@ -293,7 +293,10 @@ export class CommitSummary extends React.Component<
|
|||
/>
|
||||
|
||||
<ul className="commit-summary-meta">
|
||||
<li className="commit-summary-meta-item" aria-label="Author">
|
||||
<li
|
||||
className="commit-summary-meta-item without-truncation"
|
||||
aria-label="Author"
|
||||
>
|
||||
<AvatarStack users={this.state.avatarUsers} />
|
||||
<CommitAttribution
|
||||
gitHubRepository={this.props.repository.gitHubRepository}
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as URL from 'url'
|
|||
import * as classNames from 'classnames'
|
||||
import { UserAutocompletionProvider, IUserHit } from '../autocompletion'
|
||||
import { Editor, Doc, Position } from 'codemirror'
|
||||
import { validLoginExpression, getDotComAPIEndpoint } from '../../lib/api'
|
||||
import { getDotComAPIEndpoint } from '../../lib/api'
|
||||
import { compare } from '../../lib/compare'
|
||||
import { arrayEquals } from '../../lib/equality'
|
||||
import { OcticonSymbol } from '../octicons'
|
||||
|
@ -629,9 +629,9 @@ export class AuthorInput extends React.Component<IAuthorInputProps, {}> {
|
|||
needle
|
||||
)
|
||||
|
||||
const exactMatch =
|
||||
hits.length === 1 &&
|
||||
hits[0].username.toLowerCase() === needle.toLowerCase()
|
||||
const exactMatch = hits.some(
|
||||
hit => hit.username.toLowerCase() === needle.toLowerCase()
|
||||
)
|
||||
|
||||
const existingUsernames = new Set(this.authors.map(x => x.username))
|
||||
|
||||
|
@ -646,7 +646,7 @@ export class AuthorInput extends React.Component<IAuthorInputProps, {}> {
|
|||
hint: this.applyCompletion,
|
||||
}))
|
||||
|
||||
if (!exactMatch && needle.length > 0 && validLoginExpression.test(needle)) {
|
||||
if (!exactMatch && needle.length > 0) {
|
||||
list.push({
|
||||
text: `@${needle}`,
|
||||
username: needle,
|
||||
|
|
|
@ -5,6 +5,8 @@ import { List, SelectionSource as ListSelectionSource } from '../lib/list'
|
|||
import { TextBox } from '../lib/text-box'
|
||||
import { Row } from '../lib/row'
|
||||
|
||||
import { match, IMatch } from '../../lib/fuzzy-find'
|
||||
|
||||
/** An item in the filter list. */
|
||||
export interface IFilterListItem {
|
||||
/** The text which represents the item. This is used for filtering. */
|
||||
|
@ -31,6 +33,8 @@ interface IFlattenedGroup {
|
|||
interface IFlattenedItem<T extends IFilterListItem> {
|
||||
readonly kind: 'item'
|
||||
readonly item: T
|
||||
/** Array of indexes in `item.text` that should be highlighted */
|
||||
readonly matches: ReadonlyArray<number>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,7 +59,10 @@ interface IFilterListProps<T extends IFilterListItem> {
|
|||
readonly selectedItem: T | null
|
||||
|
||||
/** Called to render each visible item. */
|
||||
readonly renderItem: (item: T) => JSX.Element | null
|
||||
readonly renderItem: (
|
||||
item: T,
|
||||
matches: ReadonlyArray<number>
|
||||
) => JSX.Element | null
|
||||
|
||||
/** Called to render header for the group with the given identifier. */
|
||||
readonly renderGroupHeader?: (identifier: string) => JSX.Element | null
|
||||
|
@ -213,7 +220,7 @@ export class FilterList<T extends IFilterListItem> extends React.Component<
|
|||
private renderRow = (index: number) => {
|
||||
const row = this.state.rows[index]
|
||||
if (row.kind === 'item') {
|
||||
return this.props.renderItem(row.item)
|
||||
return this.props.renderItem(row.item, row.matches)
|
||||
} else if (this.props.renderGroupHeader) {
|
||||
return this.props.renderGroupHeader(row.identifier)
|
||||
} else {
|
||||
|
@ -384,9 +391,9 @@ function createStateUpdate<T extends IFilterListItem>(
|
|||
const filter = (props.filterText || '').toLowerCase()
|
||||
|
||||
for (const group of props.groups) {
|
||||
const items = group.items.filter(i => {
|
||||
return i.text.toLowerCase().includes(filter)
|
||||
})
|
||||
const items: ReadonlyArray<IMatch<T>> = filter
|
||||
? match(filter, group.items, 'text')
|
||||
: group.items.map(item => ({ score: 1, matches: [], item }))
|
||||
|
||||
if (!items.length) {
|
||||
continue
|
||||
|
@ -396,8 +403,8 @@ function createStateUpdate<T extends IFilterListItem>(
|
|||
flattenedRows.push({ kind: 'group', identifier: group.identifier })
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
flattenedRows.push({ kind: 'item', item })
|
||||
for (const { item, matches } of items) {
|
||||
flattenedRows.push({ kind: 'item', item, matches })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
43
app/src/ui/lib/highlight-text.tsx
Normal file
43
app/src/ui/lib/highlight-text.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import * as React from 'react'
|
||||
|
||||
// broken for SFCs: https://github.com/Microsoft/tslint-microsoft-contrib/issues/339
|
||||
/* tslint:disable react-unused-props-and-state */
|
||||
|
||||
interface IHighlightTextProps {
|
||||
/** The text to render */
|
||||
readonly text: string
|
||||
/** The characters in `text` to highlight */
|
||||
readonly highlight: ReadonlyArray<number>
|
||||
}
|
||||
|
||||
export const HighlightText: React.SFC<IHighlightTextProps> = ({
|
||||
text,
|
||||
highlight,
|
||||
}) => (
|
||||
<span>
|
||||
{
|
||||
text
|
||||
.split('')
|
||||
.map((ch, i): [string, boolean] => [ch, highlight.includes(i)])
|
||||
.concat([['', false]])
|
||||
.reduce(
|
||||
(state, [ch, matched], i, arr) => {
|
||||
if (matched === state.matched && i < arr.length - 1) {
|
||||
state.str += ch
|
||||
} else {
|
||||
const Component = state.matched ? 'mark' : 'span'
|
||||
state.result.push(<Component key={i}>{state.str}</Component>)
|
||||
state.str = ch
|
||||
state.matched = matched
|
||||
}
|
||||
return state
|
||||
},
|
||||
{
|
||||
matched: false,
|
||||
str: '',
|
||||
result: new Array<React.ReactElement<any>>(),
|
||||
}
|
||||
).result
|
||||
}
|
||||
</span>
|
||||
)
|
|
@ -60,7 +60,10 @@ export class RepositoriesList extends React.Component<
|
|||
IRepositoriesListProps,
|
||||
{}
|
||||
> {
|
||||
private renderItem = (item: IRepositoryListItem) => {
|
||||
private renderItem = (
|
||||
item: IRepositoryListItem,
|
||||
matches: ReadonlyArray<number>
|
||||
) => {
|
||||
const repository = item.repository
|
||||
return (
|
||||
<RepositoryListItem
|
||||
|
@ -73,7 +76,7 @@ export class RepositoriesList extends React.Component<
|
|||
onOpenInExternalEditor={this.props.onOpenInExternalEditor}
|
||||
externalEditorLabel={this.props.externalEditorLabel}
|
||||
shellLabel={this.props.shellLabel}
|
||||
filterText={this.props.filterText}
|
||||
matches={matches}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Repository } from '../../models/repository'
|
|||
import { Octicon, iconForRepository } from '../octicons'
|
||||
import { showContextualMenu, IMenuItem } from '../main-process-proxy'
|
||||
import { Repositoryish } from './group-repositories'
|
||||
import { HighlightText } from '../lib/highlight-text'
|
||||
|
||||
const defaultEditorLabel = __DARWIN__
|
||||
? 'Open in External Editor'
|
||||
|
@ -32,8 +33,8 @@ interface IRepositoryListItemProps {
|
|||
/** The label for the user's preferred shell. */
|
||||
readonly shellLabel: string
|
||||
|
||||
/** The text entered by the user to filter their repository list */
|
||||
readonly filterText: string
|
||||
/** The characters in the repository name to highlight */
|
||||
readonly matches: ReadonlyArray<number>
|
||||
}
|
||||
|
||||
/** A repository item. */
|
||||
|
@ -41,24 +42,6 @@ export class RepositoryListItem extends React.Component<
|
|||
IRepositoryListItemProps,
|
||||
{}
|
||||
> {
|
||||
private renderHighlightedName(name: string) {
|
||||
const filterText = this.props.filterText
|
||||
const matchStart = name.indexOf(filterText)
|
||||
const matchLength = filterText.length
|
||||
|
||||
if (matchStart === -1) {
|
||||
return <span>{name}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{name.substr(0, matchStart)}
|
||||
<mark>{name.substr(matchStart, matchLength)}</mark>
|
||||
{name.substr(matchStart + matchLength)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const repository = this.props.repository
|
||||
const path = repository.path
|
||||
|
@ -83,7 +66,10 @@ export class RepositoryListItem extends React.Component<
|
|||
|
||||
<div className="name">
|
||||
{prefix ? <span className="prefix">{prefix}</span> : null}
|
||||
{this.renderHighlightedName(repository.name)}
|
||||
<HighlightText
|
||||
text={repository.name}
|
||||
highlight={this.props.matches}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -96,7 +82,7 @@ export class RepositoryListItem extends React.Component<
|
|||
) {
|
||||
return (
|
||||
nextProps.repository.id !== this.props.repository.id ||
|
||||
nextProps.filterText !== this.props.filterText
|
||||
nextProps.matches !== this.props.matches
|
||||
)
|
||||
} else {
|
||||
return true
|
||||
|
|
|
@ -29,7 +29,8 @@ html {
|
|||
|
||||
// We never want the window to be scrollable, everything has to fit
|
||||
// or be placed into a scrollable container.
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
@ -58,7 +59,9 @@ body {
|
|||
}
|
||||
|
||||
:not(input):not(textarea) {
|
||||
&, &::after, &::before {
|
||||
&,
|
||||
&::after,
|
||||
&::before {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
|
@ -81,9 +84,14 @@ img {
|
|||
// margin for easier control within type scales as it avoids margin collapsing.
|
||||
//
|
||||
// From: https://github.com/twbs/bootstrap/blob/a0f10e6dcb9aef2d8e36e57f3c8b1b06034a8877/scss/_reboot.scss
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: .5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
// Regardless of platform behavior we never want buttons to be
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@import "mixins/platform";
|
||||
@import "mixins/ellipsis";
|
||||
@import "mixins/octicon-status";
|
||||
@import "mixins/textboxish";
|
||||
@import "mixins/checkboard-background";
|
||||
@import 'mixins/platform';
|
||||
@import 'mixins/ellipsis';
|
||||
@import 'mixins/octicon-status';
|
||||
@import 'mixins/textboxish';
|
||||
@import 'mixins/checkboard-background';
|
||||
|
|
|
@ -1,61 +1,61 @@
|
|||
@import "ui/app";
|
||||
@import "ui/app-menu";
|
||||
@import "ui/scroll";
|
||||
@import "ui/window/title-bar";
|
||||
@import "ui/window/app-menu-bar";
|
||||
@import "ui/window/focus";
|
||||
@import "ui/window/zoom-info";
|
||||
@import "ui/window/toast-notification";
|
||||
@import "ui/file-list";
|
||||
@import "ui/octicons";
|
||||
@import "ui/list";
|
||||
@import "ui/repository-list";
|
||||
@import "ui/changes";
|
||||
@import "ui/cloning-repository-view";
|
||||
@import "ui/diff";
|
||||
@import "ui/history";
|
||||
@import "ui/repository";
|
||||
@import "ui/resizable";
|
||||
@import "ui/toolbar/toolbar";
|
||||
@import "ui/toolbar/button";
|
||||
@import "ui/toolbar/dropdown";
|
||||
@import "ui/tab-bar";
|
||||
@import "ui/panel";
|
||||
@import "ui/popup";
|
||||
@import "ui/progress";
|
||||
@import "ui/branches";
|
||||
@import "ui/emoji";
|
||||
@import "ui/ui-view";
|
||||
@import "ui/autocompletion";
|
||||
@import "ui/welcome";
|
||||
@import "ui/foldout";
|
||||
@import "ui/preferences";
|
||||
@import "ui/path-text";
|
||||
@import "ui/configure-git-user";
|
||||
@import "ui/form";
|
||||
@import "ui/text-box";
|
||||
@import "ui/button";
|
||||
@import "ui/select";
|
||||
@import "ui/row";
|
||||
@import "ui/text-area";
|
||||
@import "ui/checkbox";
|
||||
@import "ui/errors";
|
||||
@import "ui/dialog";
|
||||
@import "ui/add-repository";
|
||||
@import "ui/discard-changes";
|
||||
@import "ui/filter-list";
|
||||
@import "ui/missing-repository-view";
|
||||
@import "ui/horizontal-rule";
|
||||
@import "ui/about";
|
||||
@import "ui/avatar";
|
||||
@import "ui/call-to-action";
|
||||
@import "ui/acknowledgements";
|
||||
@import "ui/update-notification";
|
||||
@import "ui/vertical-segmented-control";
|
||||
@import "ui/blank-slate";
|
||||
@import "ui/terms-and-conditions";
|
||||
@import "ui/ref";
|
||||
@import "ui/monospaced";
|
||||
@import 'ui/app';
|
||||
@import 'ui/app-menu';
|
||||
@import 'ui/scroll';
|
||||
@import 'ui/window/title-bar';
|
||||
@import 'ui/window/app-menu-bar';
|
||||
@import 'ui/window/focus';
|
||||
@import 'ui/window/zoom-info';
|
||||
@import 'ui/window/toast-notification';
|
||||
@import 'ui/file-list';
|
||||
@import 'ui/octicons';
|
||||
@import 'ui/list';
|
||||
@import 'ui/repository-list';
|
||||
@import 'ui/changes';
|
||||
@import 'ui/cloning-repository-view';
|
||||
@import 'ui/diff';
|
||||
@import 'ui/history';
|
||||
@import 'ui/repository';
|
||||
@import 'ui/resizable';
|
||||
@import 'ui/toolbar/toolbar';
|
||||
@import 'ui/toolbar/button';
|
||||
@import 'ui/toolbar/dropdown';
|
||||
@import 'ui/tab-bar';
|
||||
@import 'ui/panel';
|
||||
@import 'ui/popup';
|
||||
@import 'ui/progress';
|
||||
@import 'ui/branches';
|
||||
@import 'ui/emoji';
|
||||
@import 'ui/ui-view';
|
||||
@import 'ui/autocompletion';
|
||||
@import 'ui/welcome';
|
||||
@import 'ui/foldout';
|
||||
@import 'ui/preferences';
|
||||
@import 'ui/path-text';
|
||||
@import 'ui/configure-git-user';
|
||||
@import 'ui/form';
|
||||
@import 'ui/text-box';
|
||||
@import 'ui/button';
|
||||
@import 'ui/select';
|
||||
@import 'ui/row';
|
||||
@import 'ui/text-area';
|
||||
@import 'ui/checkbox';
|
||||
@import 'ui/errors';
|
||||
@import 'ui/dialog';
|
||||
@import 'ui/add-repository';
|
||||
@import 'ui/discard-changes';
|
||||
@import 'ui/filter-list';
|
||||
@import 'ui/missing-repository-view';
|
||||
@import 'ui/horizontal-rule';
|
||||
@import 'ui/about';
|
||||
@import 'ui/avatar';
|
||||
@import 'ui/call-to-action';
|
||||
@import 'ui/acknowledgements';
|
||||
@import 'ui/update-notification';
|
||||
@import 'ui/vertical-segmented-control';
|
||||
@import 'ui/blank-slate';
|
||||
@import 'ui/terms-and-conditions';
|
||||
@import 'ui/ref';
|
||||
@import 'ui/monospaced';
|
||||
@import 'ui/initialize-lfs';
|
||||
@import 'ui/ci-status';
|
||||
@import 'ui/pull-request-badge';
|
||||
|
|
|
@ -36,8 +36,11 @@
|
|||
// Typography
|
||||
//
|
||||
// Font, line-height, and color for body text, headings, and more.
|
||||
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif;
|
||||
--font-family-monospace: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||
'Helvetica Neue', Arial, sans-serif;
|
||||
--font-family-monospace: Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||
'Courier New', monospace;
|
||||
|
||||
/**
|
||||
* Font weight to use for semibold text
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "~react-virtualized/styles.css";
|
||||
@import "~codemirror/lib/codemirror.css";
|
||||
@import "~codemirror/theme/solarized.css";
|
||||
@import "~codemirror/addon/scroll/simplescrollbars.css"
|
||||
@import '~react-virtualized/styles.css';
|
||||
@import '~codemirror/lib/codemirror.css';
|
||||
@import '~codemirror/theme/solarized.css';
|
||||
@import '~codemirror/addon/scroll/simplescrollbars.css';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
@import "vendor";
|
||||
@import 'vendor';
|
||||
|
||||
@import "variables";
|
||||
@import "mixins";
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
@import "globals";
|
||||
@import "type";
|
||||
@import 'globals';
|
||||
@import 'type';
|
||||
|
||||
@import "ui"
|
||||
@import 'ui';
|
||||
|
|
|
@ -1,12 +1,26 @@
|
|||
@mixin octicon-status {
|
||||
.status {
|
||||
&-new { fill: var(--color-new); }
|
||||
&-copied { fill: var(--color-new); }
|
||||
&-modified { fill: var(--color-modified); }
|
||||
&-renamed { fill: var(--color-renamed); }
|
||||
&-deleted { fill: var(--color-deleted); }
|
||||
&-conflicted { fill: var(--color-conflicted); }
|
||||
&-new {
|
||||
fill: var(--color-new);
|
||||
}
|
||||
&-copied {
|
||||
fill: var(--color-new);
|
||||
}
|
||||
&-modified {
|
||||
fill: var(--color-modified);
|
||||
}
|
||||
&-renamed {
|
||||
fill: var(--color-renamed);
|
||||
}
|
||||
&-deleted {
|
||||
fill: var(--color-deleted);
|
||||
}
|
||||
&-conflicted {
|
||||
fill: var(--color-conflicted);
|
||||
}
|
||||
}
|
||||
|
||||
.line-endings { fill: var(--color-conflicted); }
|
||||
.line-endings {
|
||||
fill: var(--color-conflicted);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "./platform";
|
||||
@import './platform';
|
||||
|
||||
// Essentially all the styles needed to transform a text box
|
||||
// input element into something that doesn't look horrendous.
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
dialog#about {
|
||||
|
||||
.dialog-content {
|
||||
.row-component {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
p, h2 {
|
||||
p,
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,6 @@ dialog#about {
|
|||
}
|
||||
|
||||
.update-status {
|
||||
|
||||
.octicon.spin {
|
||||
// Make sure the spinner is aligned with the text.
|
||||
align-self: center;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "../mixins";
|
||||
@import '../mixins';
|
||||
|
||||
#add-repository {
|
||||
width: 350px;
|
||||
|
@ -52,8 +52,8 @@
|
|||
padding: 0;
|
||||
|
||||
.row-component:not(:last-child) {
|
||||
margin: 0;
|
||||
}
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.clone-generic-repository-content {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "../mixins";
|
||||
@import '../mixins';
|
||||
|
||||
#app-menu-foldout {
|
||||
height: 100%;
|
||||
|
@ -37,7 +37,6 @@
|
|||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
|
||||
// We want list items in previous menus to behave as if they have focus
|
||||
// even though they don't, ie we want the selected+focus state
|
||||
// to be in effect for all parent selected menu items as well as
|
||||
|
@ -68,7 +67,9 @@
|
|||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
&.disabled { opacity: 0.3; }
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@import "../mixins";
|
||||
@import '../mixins';
|
||||
|
||||
#desktop-app {
|
||||
|
||||
// This is just a dummy wrapper needed because react doesn't like
|
||||
// being installed into <body>, see https://github.com/facebook/react/issues/3207
|
||||
&-container {
|
||||
|
@ -23,6 +22,6 @@
|
|||
&-contents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,9 @@
|
|||
box-shadow: 0 0 0 1px var(--secondary-button-focus-shadow-color);
|
||||
}
|
||||
|
||||
&:disabled { opacity: 0.6; }
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.octicon {
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@import "changes/commit-message";
|
||||
@import "changes/changes-list";
|
||||
@import "changes/sidebar";
|
||||
@import "changes/undo-commit";
|
||||
@import "changes/changes-view";
|
||||
@import "changes/no-changes";
|
||||
@import 'changes/commit-message';
|
||||
@import 'changes/changes-list';
|
||||
@import 'changes/sidebar';
|
||||
@import 'changes/undo-commit';
|
||||
@import 'changes/changes-view';
|
||||
@import 'changes/no-changes';
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
align-items: center;
|
||||
|
||||
input {
|
||||
|
||||
margin: 0;
|
||||
|
||||
// Only add a right margin if there's a label attached to it
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@import "../mixins";
|
||||
@import '../mixins';
|
||||
|
||||
#cloning-repository-view {
|
||||
|
||||
/* The view's position in relation to its parent, ie full
|
||||
* width, vertically centered... */
|
||||
justify-content: center;
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
@import "../mixins";
|
||||
@import "dialogs/merge";
|
||||
@import "dialogs/publish-repository";
|
||||
@import "dialogs/repository-settings";
|
||||
@import '../mixins';
|
||||
@import 'dialogs/merge';
|
||||
@import 'dialogs/publish-repository';
|
||||
@import 'dialogs/repository-settings';
|
||||
|
||||
// The styles herein attempt to follow a flow where margins are only applied
|
||||
// to the bottom of elements (with the exception of the last child). This to
|
||||
// allow easy layout using generalized components and elements such as <Row>
|
||||
// and <p>.
|
||||
dialog {
|
||||
|
||||
// These are custom version of the alert and stop octicons that have been
|
||||
// scaled and adjusted to render crisply at 24px.
|
||||
//
|
||||
|
@ -38,7 +37,6 @@ dialog {
|
|||
// The modal class here is the transition name for the react css transition
|
||||
// group which allows us to apply an animation when the popup appears.
|
||||
&.modal {
|
||||
|
||||
&-enter {
|
||||
opacity: 1;
|
||||
transform: scale(0.75);
|
||||
|
@ -74,7 +72,7 @@ dialog {
|
|||
opacity: 0.01;
|
||||
transform: scale(0.25);
|
||||
transition: opacity 100ms ease-in,
|
||||
transform 100ms var(--easing-ease-in-back);
|
||||
transform 100ms var(--easing-ease-in-back);
|
||||
|
||||
&::backdrop {
|
||||
opacity: 0.01;
|
||||
|
@ -143,7 +141,9 @@ dialog {
|
|||
// Let the button deal with all mouse events.
|
||||
// Without this the octicon resets the cursor when
|
||||
// hovering over the <path>.
|
||||
.octicon { pointer-events: none; }
|
||||
.octicon {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
|
@ -155,7 +155,8 @@ dialog {
|
|||
}
|
||||
}
|
||||
|
||||
&.warning, &.error {
|
||||
&.warning,
|
||||
&.error {
|
||||
.dialog-content {
|
||||
position: relative;
|
||||
margin-left: var(--spacing-double);
|
||||
|
@ -167,7 +168,10 @@ dialog {
|
|||
// Ensure that the dialog contents always have room for the icon,
|
||||
// account for two double spacers at top and bottom plus the 5px
|
||||
// icon offset (margin-top) and the size of the icon itself.
|
||||
min-height: calc(var(--spacing-double) * 2 + var(--spacing-half) + var(--dialog-icon-size));
|
||||
min-height: calc(
|
||||
var(--spacing-double) * 2 + var(--spacing-half) +
|
||||
var(--dialog-icon-size)
|
||||
);
|
||||
|
||||
// We're creating an opaque 24 by 24px div with the background color
|
||||
// that we want the icon to appear in and then apply the icon path
|
||||
|
@ -220,7 +224,8 @@ dialog {
|
|||
}
|
||||
}
|
||||
|
||||
h2, h3 {
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--spacing);
|
||||
|
@ -230,15 +235,22 @@ dialog {
|
|||
}
|
||||
}
|
||||
|
||||
h2 { font-size: var(--font-size-md); }
|
||||
h3 { font-size: var(--font-size); }
|
||||
h2 {
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
h3 {
|
||||
font-size: var(--font-size);
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
ul,
|
||||
ol {
|
||||
margin-top: 0;
|
||||
padding-left: var(--spacing-double);
|
||||
list-style-position: outside;
|
||||
|
||||
&:last-child { margin-bottom: 0; }
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: var(--spacing);
|
||||
|
@ -248,7 +260,6 @@ dialog {
|
|||
}
|
||||
|
||||
.dialog-footer {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
@ -316,12 +327,24 @@ dialog {
|
|||
}
|
||||
}
|
||||
|
||||
&#preferences { width: 450px; }
|
||||
&#about { width: 450px; }
|
||||
&#create-repository { width: 400px; }
|
||||
&#create-branch { width: 400px; }
|
||||
&#push-branch-commits { width: 450px; }
|
||||
&#publish-branch { width: 450px; }
|
||||
&#preferences {
|
||||
width: 450px;
|
||||
}
|
||||
&#about {
|
||||
width: 450px;
|
||||
}
|
||||
&#create-repository {
|
||||
width: 400px;
|
||||
}
|
||||
&#create-branch {
|
||||
width: 400px;
|
||||
}
|
||||
&#push-branch-commits {
|
||||
width: 450px;
|
||||
}
|
||||
&#publish-branch {
|
||||
width: 450px;
|
||||
}
|
||||
&#generic-git-auth {
|
||||
width: 450px;
|
||||
}
|
||||
|
@ -343,13 +366,16 @@ dialog {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.forgot-password-row, .what-is-this-row {
|
||||
.forgot-password-row,
|
||||
.what-is-this-row {
|
||||
font-size: var(--font-size-sm);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
&#add-existing-repository { width: 400px; }
|
||||
&#add-existing-repository {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
&#initialize-lfs {
|
||||
width: 400px;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@import "../mixins";
|
||||
@import '../mixins';
|
||||
|
||||
.file-list {
|
||||
|
||||
// this value affects virtualized lists, and without it
|
||||
// you'll see react-virtualized just skip rendering
|
||||
// as the available vertical space is computed as zero
|
||||
|
@ -33,7 +32,10 @@
|
|||
width: 20px;
|
||||
}
|
||||
|
||||
input, .status { flex-shrink: 0; }
|
||||
input,
|
||||
.status {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.path {
|
||||
display: flex;
|
||||
|
@ -48,6 +50,8 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.octicon { vertical-align: text-bottom; }
|
||||
.octicon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
z-index: var(--foldout-z-index);
|
||||
|
||||
.overlay {
|
||||
|
||||
// No focus styles whatsovever for the overlay.
|
||||
// The overlay has a tab index of -1 so that clicking on it
|
||||
// won't immediately trigger the lost focus event on the app
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "history/history";
|
||||
@import "history/commit-list";
|
||||
@import "history/commit-summary";
|
||||
@import "history/file-list";
|
||||
@import 'history/history';
|
||||
@import 'history/commit-list';
|
||||
@import 'history/commit-summary';
|
||||
@import 'history/file-list';
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@import "../mixins";
|
||||
@import '../mixins';
|
||||
|
||||
#missing-repository-view {
|
||||
|
||||
/* The view's position in relation to its parent, ie full
|
||||
* width, vertically centered... */
|
||||
justify-content: center;
|
||||
|
|
|
@ -3,5 +3,7 @@
|
|||
.path-text-component {
|
||||
@include ellipsis;
|
||||
|
||||
.dirname { color: var(--text-secondary-color); }
|
||||
.dirname {
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#preferences {
|
||||
.accounts-tab {
|
||||
|
||||
.account-info {
|
||||
.avatar {
|
||||
// 32px for the image + 2 on each side for the base border.
|
||||
|
|
|
@ -12,9 +12,13 @@ progress {
|
|||
}
|
||||
|
||||
&:indeterminate {
|
||||
background-image:
|
||||
-webkit-linear-gradient(-45deg, transparent 33%, var(--text-color) 33%,
|
||||
var(--text-color) 66%, transparent 66%);
|
||||
background-image: -webkit-linear-gradient(
|
||||
-45deg,
|
||||
transparent 33%,
|
||||
var(--text-color) 33%,
|
||||
var(--text-color) 66%,
|
||||
transparent 66%
|
||||
);
|
||||
background-size: 25px 10px, 100% 100%, 100% 100%;
|
||||
|
||||
-webkit-animation: progress-indeterminate-animation 5s linear infinite;
|
||||
|
@ -22,5 +26,7 @@ progress {
|
|||
}
|
||||
|
||||
@-webkit-keyframes progress-indeterminate-animation {
|
||||
100% { background-position: 100px 0px; }
|
||||
100% {
|
||||
background-position: 100px 0px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
@import "../mixins";
|
||||
@import '../mixins';
|
||||
|
||||
/** A React component holding the currently selected repository */
|
||||
#repository {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.resizable-component {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "../mixins";
|
||||
@import '../mixins';
|
||||
|
||||
@include win32-context {
|
||||
// Windows native scrollbars are just too outdated to look at so we'll
|
||||
|
@ -50,7 +50,8 @@
|
|||
}
|
||||
|
||||
// When someone hovers over, or presses the bar we'll expand it to 8px
|
||||
&:hover, &:active {
|
||||
&:hover,
|
||||
&:active {
|
||||
border-width: 1px;
|
||||
background-color: var(--scroll-bar-thumb-background-color-active);
|
||||
cursor: pointer;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "../mixins";
|
||||
@import '../mixins';
|
||||
|
||||
.select-component {
|
||||
display: flex;
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.switch &-item {
|
||||
&.switch &-item {
|
||||
// Reset styles from global buttons
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
|
|
|
@ -5,7 +5,12 @@
|
|||
height: 250px;
|
||||
overflow: scroll;
|
||||
|
||||
p, ol, ul, li, h2, h3 {
|
||||
p,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
h2,
|
||||
h3 {
|
||||
cursor: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "../mixins";
|
||||
@import '../mixins';
|
||||
|
||||
.text-box-component {
|
||||
display: flex;
|
||||
|
|
|
@ -23,23 +23,23 @@
|
|||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: var(--spacing);
|
||||
border: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
right: var(--spacing);
|
||||
border: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
|
||||
color: var(--text-secondary-color);
|
||||
color: var(--text-secondary-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@import "../mixins";
|
||||
@import '../mixins';
|
||||
|
||||
fieldset.vertical-segmented-control {
|
||||
|
||||
// Reset styles for fieldset
|
||||
border: none;
|
||||
margin: 0;
|
||||
|
|
|
@ -53,7 +53,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
input, button {
|
||||
input,
|
||||
button {
|
||||
font-size: var(--font-size-md);
|
||||
height: auto;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "../../mixins";
|
||||
@import '../../mixins';
|
||||
|
||||
#changes-list {
|
||||
min-height: 0;
|
||||
|
@ -35,10 +35,10 @@
|
|||
@include ellipsis;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
input[type='checkbox'] {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ dialog#merge {
|
|||
}
|
||||
|
||||
.dialog-content {
|
||||
|
||||
padding: 0;
|
||||
|
||||
.filter-field-row {
|
||||
|
@ -31,14 +30,15 @@ dialog#merge {
|
|||
.list-item {
|
||||
padding: 0 var(--spacing-double);
|
||||
|
||||
.filter-list-group-header, .branches-list-item {
|
||||
.filter-list-group-header,
|
||||
.branches-list-item {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
button[type=submit] {
|
||||
button[type='submit'] {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
padding: var(--spacing-half);
|
||||
|
|
|
@ -20,13 +20,14 @@
|
|||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: -5px;
|
||||
margin-top: -4px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
.description {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
@import "../../mixins";
|
||||
@import '../../mixins';
|
||||
|
||||
/** A React component holding the selected commit's detailed information */
|
||||
#commit-summary {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
@ -24,7 +23,6 @@
|
|||
}
|
||||
|
||||
&.expanded {
|
||||
|
||||
.commit-summary-description-container {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
@ -80,8 +78,14 @@
|
|||
flex: 1;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0px, rgba(255, 255, 255, 0) 40px, rgba(255, 255, 255, 0.5) 40px, white 60px);
|
||||
content: '';
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0) 0px,
|
||||
rgba(255, 255, 255, 0) 40px,
|
||||
rgba(255, 255, 255, 0.5) 40px,
|
||||
white 60px
|
||||
);
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
@ -92,11 +96,14 @@
|
|||
// Enable text selection inside the title and description elements.
|
||||
&-title,
|
||||
&-description {
|
||||
span, a {
|
||||
span,
|
||||
a {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
span { cursor: text; }
|
||||
span {
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
|
||||
&-description {
|
||||
|
@ -114,11 +121,15 @@
|
|||
padding: 0 var(--spacing) var(--spacing);
|
||||
}
|
||||
|
||||
&-meta-item:not(.without-truncation) {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
&-meta-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 0;
|
||||
|
||||
@include ellipsis;
|
||||
margin-right: var(--spacing);
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "../../mixins";
|
||||
@import '../../mixins';
|
||||
|
||||
/** A React component holding the currently selected repository's history */
|
||||
#history {
|
||||
|
@ -31,4 +31,16 @@
|
|||
// look right alongside blank diff
|
||||
border-right: var(--base-border);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
&.empty,
|
||||
&.renamed,
|
||||
&.binary {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.toolbar-button {
|
||||
|
||||
// Make sure the contents shrink beyond their intrinsic width
|
||||
// See https://css-tricks.com/flexbox-truncated-text/
|
||||
min-width: 0;
|
||||
|
@ -17,7 +16,7 @@
|
|||
// explicitly use > here to only target the direct descendant button since
|
||||
// there might be buttons in foldouts which would otherwise be affected
|
||||
// as well.
|
||||
&>button {
|
||||
& > button {
|
||||
// Reset styles from global buttons
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
|
@ -28,7 +27,9 @@
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&:active { box-shadow: none; }
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: var(--toolbar-button-focus-background-color);
|
||||
|
@ -74,8 +75,7 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
&>button {
|
||||
|
||||
& > button {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
|
@ -124,7 +124,10 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.title, .description { @include ellipsis }
|
||||
.title,
|
||||
.description {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
|
@ -135,7 +138,7 @@
|
|||
background: var(--toolbar-button-progress-color);
|
||||
transform-origin: left;
|
||||
pointer-events: none;
|
||||
transition: transform .3s var(--easing-ease-out-quint);
|
||||
transition: transform 0.3s var(--easing-ease-out-quint);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,6 +147,8 @@
|
|||
// progress we want the text to be slightly legible so we'll make it
|
||||
// opaque. Since a toolbar button with progress also shows a spinner
|
||||
// there's plenty of indication that it can't be used.
|
||||
&:disabled { opacity: 1; }
|
||||
&:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
.toolbar-dropdown {
|
||||
|
||||
// Make sure the contents shrink beyond their intrinsic width
|
||||
// See https://css-tricks.com/flexbox-truncated-text/
|
||||
min-width: 0;
|
||||
|
||||
&>.toolbar-button {
|
||||
& > .toolbar-button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.open {
|
||||
&>.toolbar-button > button {
|
||||
& > .toolbar-button > button {
|
||||
color: var(--toolbar-button-active-color);
|
||||
background-color: var(--toolbar-button-active-background-color);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "../../mixins";
|
||||
@import '../../mixins';
|
||||
|
||||
/** A React component holding the main application toolbar component. */
|
||||
#desktop-app-toolbar {
|
||||
|
@ -25,17 +25,21 @@
|
|||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
|
||||
&>:last-child {
|
||||
& > :last-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
&.push-pull-button { width: 230px; }
|
||||
&.push-pull-button {
|
||||
width: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-dropdown {
|
||||
&.branch-button { width: 230px; }
|
||||
&.branch-button {
|
||||
width: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
|
@ -45,10 +49,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
|
||||
@keyframes spin {
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation:spin 1s linear infinite;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.ahead-behind {
|
||||
|
@ -78,7 +87,9 @@
|
|||
|
||||
// Only add left margin if both ahead and behind are
|
||||
// showing at the same time.
|
||||
&:nth-child(2) { margin-left: var(--spacing-half); }
|
||||
&:nth-child(2) {
|
||||
margin-left: var(--spacing-half);
|
||||
}
|
||||
|
||||
// We're using arrowSmallUp and arrowSmallDown which are
|
||||
// both exactly 6px wide. Let's use that so that spacing
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
display: flex;
|
||||
|
||||
.toolbar-button > button {
|
||||
padding: 0 var(--spacing);
|
||||
padding: 0 var(--spacing);
|
||||
border: 0;
|
||||
|
||||
.access-key.highlight {
|
||||
|
@ -13,7 +13,8 @@
|
|||
.toolbar-dropdown:not(.open) > .toolbar-button > button {
|
||||
color: var(--toolbar-button-secondary-color);
|
||||
|
||||
&:hover,&:focus {
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--toolbar-button-color);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "../../mixins";
|
||||
@import '../../mixins';
|
||||
|
||||
#desktop-app-title-bar {
|
||||
-webkit-app-region: drag;
|
||||
|
@ -11,7 +11,7 @@
|
|||
|
||||
@include darwin {
|
||||
height: var(--darwin-title-bar-height);
|
||||
background: linear-gradient(to bottom, #3b3f46 0%,#2b2e33 100%);
|
||||
background: linear-gradient(to bottom, #3b3f46 0%, #2b2e33 100%);
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,6 @@
|
|||
}
|
||||
|
||||
.resize-handle {
|
||||
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
|
@ -50,7 +49,6 @@
|
|||
// automatically even for borderless window so we only render
|
||||
// controls on Windows.
|
||||
.window-controls {
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
|
@ -77,7 +75,9 @@
|
|||
background-color: transparent;
|
||||
transition: background-color 0.25s ease;
|
||||
|
||||
&:focus { outline: none; }
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #888;
|
||||
|
@ -110,7 +110,9 @@
|
|||
}
|
||||
|
||||
/* https://css-tricks.com/cascading-svg-fill-color/ */
|
||||
svg { fill: currentColor; }
|
||||
svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,12 +37,18 @@
|
|||
}
|
||||
|
||||
.toast-animation {
|
||||
&-appear { transform: scale(0.25); opacity: 0.1; }
|
||||
&-appear {
|
||||
transform: scale(0.25);
|
||||
opacity: 0.1;
|
||||
}
|
||||
&-appear-active {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
transition: all 100ms ease-out;
|
||||
}
|
||||
|
||||
&-leave-active { opacity: 0; transition: all 250ms ease-out; }
|
||||
&-leave-active {
|
||||
opacity: 0;
|
||||
transition: all 250ms ease-out;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
top: 0;
|
||||
left: 0;
|
||||
|
||||
&>div {
|
||||
& > div {
|
||||
padding: var(--spacing);
|
||||
min-width: 100px;
|
||||
background: rgba($gray-900, 0.6);
|
||||
|
@ -27,15 +27,25 @@
|
|||
}
|
||||
|
||||
.zoom-in {
|
||||
&-appear { transform: scale(0.25); opacity: 0; }
|
||||
&-appear {
|
||||
transform: scale(0.25);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.zoom-out {
|
||||
&-appear { transform: scale(1.75); opacity: 0; }
|
||||
&-appear {
|
||||
transform: scale(1.75);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.zoom-in, .zoom-out {
|
||||
&-leave-active { opacity: 0; transition: opacity 100ms ease-out; }
|
||||
.zoom-in,
|
||||
.zoom-out {
|
||||
&-leave-active {
|
||||
opacity: 0;
|
||||
transition: opacity 100ms ease-out;
|
||||
}
|
||||
&-appear-active {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
|
|
40
app/test/unit/pull-request-database-test.ts
Normal file
40
app/test/unit/pull-request-database-test.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { expect } from 'chai'
|
||||
|
||||
import {
|
||||
PullRequestDatabase,
|
||||
IPullRequestStatus,
|
||||
} from '../../src/lib/databases'
|
||||
|
||||
describe('PullRequestDatabase', () => {
|
||||
it("adds statuses key to records that don't have one on upgrade", async () => {
|
||||
const databaseName = 'TestPullRequestDatabase'
|
||||
|
||||
let database = new PullRequestDatabase(databaseName, 3)
|
||||
await database.delete()
|
||||
await database.open()
|
||||
|
||||
const prStatus: IPullRequestStatus = {
|
||||
pullRequestId: 1,
|
||||
state: 'success',
|
||||
totalCount: 1,
|
||||
sha: 'sha',
|
||||
}
|
||||
await database.pullRequestStatus.add(prStatus)
|
||||
const prStatusFromDb = await database.pullRequestStatus.get(1)
|
||||
expect(prStatusFromDb).to.not.be.undefined
|
||||
expect(prStatusFromDb!.pullRequestId).to.equal(prStatus.pullRequestId)
|
||||
|
||||
database.close()
|
||||
database = new PullRequestDatabase(databaseName, 4)
|
||||
await database.open()
|
||||
|
||||
const upgradedPrStatusFromDb = await database.pullRequestStatus.get(1)
|
||||
expect(upgradedPrStatusFromDb).is.not.undefined
|
||||
expect(upgradedPrStatusFromDb!.pullRequestId).to.equal(
|
||||
prStatus.pullRequestId
|
||||
)
|
||||
expect(upgradedPrStatusFromDb!.statuses).is.not.undefined
|
||||
|
||||
await database.delete()
|
||||
})
|
||||
})
|
|
@ -8,7 +8,6 @@ import { expect } from 'chai'
|
|||
import { RepositorySettingsStore } from '../../src/lib/stores'
|
||||
import { setupEmptyRepository } from '../helpers/repositories'
|
||||
import { getStatus } from '../../src/lib/git'
|
||||
import { Repository } from '../../src/models/repository'
|
||||
import { pathExists } from '../../src/lib/file-system'
|
||||
|
||||
describe('RepositorySettingsStore', () => {
|
||||
|
@ -48,13 +47,10 @@ describe('RepositorySettingsStore', () => {
|
|||
expect(files.length).to.equal(0)
|
||||
})
|
||||
|
||||
describe('autocrlf and safecrlf', () => {
|
||||
let repo: Repository
|
||||
let sut: RepositorySettingsStore
|
||||
|
||||
beforeEach(async () => {
|
||||
repo = await setupEmptyRepository()
|
||||
sut = new RepositorySettingsStore(repo)
|
||||
describe('autocrlf and safecrlf are true', () => {
|
||||
it('appends CRLF to file', async () => {
|
||||
const repo = await setupEmptyRepository()
|
||||
const sut = new RepositorySettingsStore(repo)
|
||||
|
||||
await GitProcess.exec(
|
||||
['config', '--local', 'core.autocrlf', 'true'],
|
||||
|
@ -64,9 +60,7 @@ describe('RepositorySettingsStore', () => {
|
|||
['config', '--local', 'core.safecrlf', 'true'],
|
||||
repo.path
|
||||
)
|
||||
})
|
||||
|
||||
it('appends newline to file', async () => {
|
||||
const path = repo.path
|
||||
|
||||
await sut.saveGitIgnore('node_modules')
|
||||
|
@ -82,4 +76,34 @@ describe('RepositorySettingsStore', () => {
|
|||
expect(contents!.endsWith('\r\n'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('autocrlf and safecrlf are unset', () => {
|
||||
it('appends LF to file', async () => {
|
||||
const repo = await setupEmptyRepository()
|
||||
const sut = new RepositorySettingsStore(repo)
|
||||
|
||||
// ensure this repository only ever sticks to LF
|
||||
await GitProcess.exec(['config', '--local', 'core.eol', 'lf'], repo.path)
|
||||
|
||||
// do not do any conversion of line endings when committing
|
||||
await GitProcess.exec(
|
||||
['config', '--local', 'core.autocrlf', 'input'],
|
||||
repo.path
|
||||
)
|
||||
|
||||
const path = repo.path
|
||||
|
||||
await sut.saveGitIgnore('node_modules')
|
||||
await GitProcess.exec(['add', '.gitignore'], path)
|
||||
|
||||
const commit = await GitProcess.exec(
|
||||
['commit', '-m', 'create the ignore file'],
|
||||
path
|
||||
)
|
||||
const contents = await sut.readGitIgnore()
|
||||
|
||||
expect(commit.exitCode).to.equal(0)
|
||||
expect(contents!.endsWith('\n'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
60
app/test/unit/script/version-test.ts
Normal file
60
app/test/unit/script/version-test.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { expect } from 'chai'
|
||||
|
||||
import { getNextVersionNumber } from '../../../../script/draft-release/version'
|
||||
|
||||
describe('getNextVersionNumber', () => {
|
||||
describe('production', () => {
|
||||
const channel = 'production'
|
||||
|
||||
it('increments the patch number', () => {
|
||||
expect(getNextVersionNumber('1.0.1', channel)).to.equal('1.0.2')
|
||||
})
|
||||
|
||||
describe("doesn't care for", () => {
|
||||
it('beta versions', () => {
|
||||
const version = '1.0.1-beta1'
|
||||
expect(() => getNextVersionNumber(version, channel)).to.throw(
|
||||
`Unable to draft production release using beta version '${version}'`
|
||||
)
|
||||
})
|
||||
it('test versions', () => {
|
||||
const version = '1.0.1-test42'
|
||||
expect(() => getNextVersionNumber('1.0.1-test42', channel)).to.throw(
|
||||
`Unable to draft production release using test version '${version}'`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('beta', () => {
|
||||
const channel = 'beta'
|
||||
|
||||
describe('when a beta version is used', () => {
|
||||
it('the beta tag is incremented', () => {
|
||||
expect(getNextVersionNumber('1.1.2-beta3', channel)).to.equal(
|
||||
'1.1.2-beta4'
|
||||
)
|
||||
})
|
||||
it('handles multiple digits', () => {
|
||||
expect(getNextVersionNumber('1.1.2-beta99', channel)).to.equal(
|
||||
'1.1.2-beta100'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a production version is used', () => {
|
||||
it('increments the patch and returns the first beta', () => {
|
||||
expect(getNextVersionNumber('1.0.1', channel)).to.equal('1.0.2-beta1')
|
||||
})
|
||||
})
|
||||
|
||||
describe("doesn't care for", () => {
|
||||
it('test versions', () => {
|
||||
const version = '1.0.1-test1'
|
||||
expect(() => getNextVersionNumber(version, channel)).to.throw(
|
||||
`Unable to draft beta release using test version '${version}'`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -155,7 +155,13 @@ co@^4.6.0:
|
|||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
|
||||
codemirror@^5.31.0:
|
||||
codemirror-mode-elixir@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/codemirror-mode-elixir/-/codemirror-mode-elixir-1.1.1.tgz#cc5b79bf5f93b6da426e32364a673a681391416c"
|
||||
dependencies:
|
||||
codemirror "^5.20.2"
|
||||
|
||||
codemirror@^5.20.2, codemirror@^5.31.0:
|
||||
version "5.33.0"
|
||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.33.0.tgz#462ad9a6fe8d38b541a9536a3997e1ef93b40c6a"
|
||||
|
||||
|
@ -434,6 +440,10 @@ fs.realpath@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
|
||||
fuzzaldrin-plus@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/fuzzaldrin-plus/-/fuzzaldrin-plus-0.6.0.tgz#832f6489fbe876769459599c914a670ec22947ee"
|
||||
|
||||
getpass@^0.1.1:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
|
||||
|
|
|
@ -1,5 +1,27 @@
|
|||
{
|
||||
"releases": {
|
||||
"1.0.14-beta4": [
|
||||
"[New] Syntax highlighting for Elixir files - #3774. Thanks @joaovitoras!",
|
||||
"[Fixed] Crash when unable to launch shell - #3954",
|
||||
"[Fixed] Support legacy usernames as co-authors - #3897",
|
||||
"[Improved] Enable fuzzy search in the repository, branch, PR, and clone FilterLists - #911. Thanks @j-f1!",
|
||||
"[Improved] Tidy up commit summary and description layout in commit list - #3922. Thanks @willnode!"
|
||||
],
|
||||
"1.0.14-beta3": [
|
||||
"[Added] Add TextMate support for macOS - #3910. Thanks @caiofbpa!",
|
||||
"[Fixed] Handle Git errors when .gitmodules are malformed - #3912",
|
||||
"[Fixed] Clear repository filter when switching tabs - #3787. Thanks @reyronald!",
|
||||
"[Fixed] Prevent duplicate entries in co-author autocomplete list - #3887",
|
||||
"[Improved] Show progress when initializing remote for fork - #3953"
|
||||
],
|
||||
"1.0.14-beta2": [
|
||||
"[Added] Add RubyMine support for macOS - #3883. Thanks @gssbzn!",
|
||||
"[Fixed] Allow window to accept single click on focus - #3843",
|
||||
"[Fixed] Expanded avatar list hidden behind commit details - #3884",
|
||||
"[Fixed] Renames not detected when viewing commit diffs - #3673",
|
||||
"[Fixed] Ignore action assumes CRLF when core.autocrlf is unset - #3514",
|
||||
"[Improved] Use smaller default size when rendering Gravatar avatars - #3911"
|
||||
],
|
||||
"1.0.14-beta1": [
|
||||
"[New] Commit together with co-authors - #3879"
|
||||
],
|
||||
|
|
|
@ -12,12 +12,77 @@ We have three channels to which we can release: `production`, `beta`, and `test`
|
|||
|
||||
## The Process
|
||||
|
||||
1. Ensure the release notes for `$version` in [`changelog.json`](../../changelog.json) are up-to-date.
|
||||
1. Bump `version` in [`app/package.json`](../../app/package.json) to `$version`.
|
||||
1. Commit & push the changes.
|
||||
1. Run `.release! desktop/YOUR_BRANCH to {production|beta|test}`.
|
||||
* We're using `.release` with a bang so that we don't have to wait for any current CI on the branch to finish. This might feel a little wrong, but it's OK since making the release itself will also run CI.
|
||||
1. If you're releasing a production update, release a beta update for the next version too, so that beta users are on the latest release.
|
||||
From a clean working directory, set the `GITHUB_ACCESS_TOKEN` environment variable to a valid [Personal Access Token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) and run:
|
||||
|
||||
```shellsession
|
||||
$ yarn draft-release (production|beta)
|
||||
```
|
||||
|
||||
This command will then explain the next steps:
|
||||
|
||||
```shellsession
|
||||
Here's what you should do next:
|
||||
|
||||
1. Ensure the app/package.json 'version' is set to '1.0.14-beta2'
|
||||
2. Add this to changelog.json as a starting point:
|
||||
{
|
||||
"1.0.14-beta2": [
|
||||
"[???] Add RubyMine support for macOS - #3883. Thanks @gssbzn!",
|
||||
"[???] Allow window to accept single click on focus - #3843",
|
||||
"[???] Drop unnecessary comments before issue template - #3906",
|
||||
"[???] First-class changelog script for generating release notes - #3888",
|
||||
"[???] Fix expanded avatar stack overflow - #3884",
|
||||
"[???] Switch to a saner default gravatar size - #3911",
|
||||
"[Fixed] Add a repository settings store - #934",
|
||||
"[Fixed] Ensure renames are detected when viewing commit diffs - #3673",
|
||||
"[Fixed] Line endings are hard, lets go shopping - #3514",
|
||||
]
|
||||
}
|
||||
|
||||
3. Update the release notes so they make sense and only contain user-facing changes
|
||||
4. Commit the changes and push them to GitHub
|
||||
5. Read this to perform the release: https://github.com/desktop/desktop/blob/master/docs/process/releasing-updates.md
|
||||
```
|
||||
|
||||
To walk through this:
|
||||
|
||||
- the script works out the next version from what was previously published, based on the channel
|
||||
- you should ensure the `version` in `app/package.json` is set to the new version
|
||||
- then, take the draft changelog generated by the script and add it to the `releases` element in `changelog.json`
|
||||
|
||||
The draft changelog covers everything that's been merged, and probably need some love. It's your job from here to:
|
||||
|
||||
- remove any entries of contributions that don't affect the end user
|
||||
- for issues prefixed with `[???]`, look at the linked PR or issue and change the prefix to one of `[New]`, `[Fixed]`, `[Improved]`, `[Added]` or `[Removed]` based on what best represents the change
|
||||
- edit the remaining entries so they make sense
|
||||
- sort the entries so that the prefixes are ordered in this way: `[New]`, `[Fixed]`, `[Improved]`, `[Added]`, `[Removed]`
|
||||
|
||||
Here's an example of the previous changelog draft afterit has been edited:
|
||||
|
||||
```json
|
||||
{
|
||||
"1.0.14-beta2": [
|
||||
"[Added] Add RubyMine support for macOS - #3883. Thanks @gssbzn!",
|
||||
"[Fixed] Allow window to accept single click on focus - #3843",
|
||||
"[Fixed] Expanded avatar list hidden behind commit details - #3884",
|
||||
"[Fixed] Renames not detected when viewing commit diffs - #3673",
|
||||
"[Fixed] Ignore action assumes CRLF when core.autocrlf is unset - #3514",
|
||||
"[Improved] Use smaller default size when rendering Gravatar avatars - #3911",
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Once you are happy with those changes, commit and push them to GitHub. Get the others on the team to :thumbsup: them if you're not sure.
|
||||
|
||||
When you feel ready to start the deployment, run this command in Chat:
|
||||
|
||||
```
|
||||
.release! desktop/YOUR_BRANCH to {production|beta|test}
|
||||
```
|
||||
|
||||
We're using `.release` with a bang so that we don't have to wait for any current CI on the branch to finish. This might feel a little wrong, but it's OK since making the release itself will also run CI.
|
||||
|
||||
If you're releasing a production update, release a beta update for the next version too, so that beta users are on the latest release.
|
||||
|
||||
## Error Reporting
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue