mirror of
https://github.com/desktop/desktop
synced 2024-09-12 21:01:16 +00:00
Merge branch 'development' into upgrade-octicons
This commit is contained in:
commit
edac8e0f5e
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -21,7 +21,7 @@ jobs:
|
|||
friendlyName: macOS
|
||||
- os: windows-2019
|
||||
friendlyName: Windows
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
|
@ -79,4 +79,4 @@ jobs:
|
|||
S3_KEY: ${{ secrets.S3_KEY }}
|
||||
S3_SECRET: ${{ secrets.S3_SECRET }}
|
||||
S3_BUCKET: github-desktop
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 10
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"productName": "GitHub Desktop",
|
||||
"bundleID": "com.github.GitHubClient",
|
||||
"companyName": "GitHub, Inc.",
|
||||
"version": "2.5.4-beta2",
|
||||
"version": "2.5.4-beta4",
|
||||
"main": "./main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -49,7 +49,7 @@
|
|||
"react": "^16.8.4",
|
||||
"react-css-transition-replace": "^3.0.3",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-transition-group": "^1.2.0",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtualized": "^9.20.0",
|
||||
"registry-js": "^1.4.0",
|
||||
"source-map-support": "^0.4.15",
|
||||
|
|
|
@ -72,6 +72,8 @@ const extensionModes: ReadonlyArray<IModeDefinition> = [
|
|||
{
|
||||
install: () => import('codemirror/mode/htmlembedded/htmlembedded'),
|
||||
mappings: {
|
||||
'.aspx': 'application/x-aspx',
|
||||
'.cshtml': 'application/x-aspx',
|
||||
'.jsp': 'application/x-jsp',
|
||||
},
|
||||
},
|
||||
|
@ -114,6 +116,15 @@ const extensionModes: ReadonlyArray<IModeDefinition> = [
|
|||
'.vbproj': 'text/xml',
|
||||
'.svg': 'text/xml',
|
||||
'.resx': 'text/xml',
|
||||
'.props': 'text/xml',
|
||||
'.targets': 'text/xml',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/diff/diff'),
|
||||
mappings: {
|
||||
'.diff': 'text/x-diff',
|
||||
'.patch': 'text/x-diff',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -527,7 +538,7 @@ function getInnerModeName(
|
|||
* @param stream The StringStream for the current line
|
||||
* @param state The current mode state (if any)
|
||||
* @param addModeClass Whether or not to append the current (inner) mode name
|
||||
* as an extra CSS clas to the token, indicating the mode
|
||||
* as an extra CSS class to the token, indicating the mode
|
||||
* that produced it, prefixed with "cm-m-". For example,
|
||||
* tokens from the XML mode will get the cm-m-xml class.
|
||||
*/
|
||||
|
|
|
@ -252,7 +252,21 @@ export interface IAPIIssue {
|
|||
}
|
||||
|
||||
/** The combined state of a ref. */
|
||||
export type APIRefState = 'failure' | 'pending' | 'success'
|
||||
export type APIRefState = 'failure' | 'pending' | 'success' | 'error'
|
||||
|
||||
/** The overall status of a check run */
|
||||
export type APICheckStatus = 'queued' | 'in_progress' | 'completed'
|
||||
|
||||
/** The conclusion of a completed check run */
|
||||
export type APICheckConclusion =
|
||||
| 'action_required'
|
||||
| 'cancelled'
|
||||
| 'timed_out'
|
||||
| 'failure'
|
||||
| 'neutral'
|
||||
| 'success'
|
||||
| 'skipped'
|
||||
| 'stale'
|
||||
|
||||
/**
|
||||
* The API response for a combined view of a commit
|
||||
|
@ -273,6 +287,30 @@ export interface IAPIRefStatus {
|
|||
readonly statuses: ReadonlyArray<IAPIRefStatusItem>
|
||||
}
|
||||
|
||||
export interface IAPIRefCheckRun {
|
||||
readonly id: number
|
||||
readonly url: string
|
||||
readonly status: APICheckStatus
|
||||
readonly conclusion: APICheckConclusion | null
|
||||
readonly name: string
|
||||
readonly output: IAPIRefCheckRunOutput
|
||||
readonly check_suite: IAPIRefCheckRunCheckSuite
|
||||
}
|
||||
|
||||
// NB. Only partially mapped
|
||||
export interface IAPIRefCheckRunOutput {
|
||||
readonly title: string | null
|
||||
}
|
||||
|
||||
export interface IAPIRefCheckRunCheckSuite {
|
||||
readonly id: number
|
||||
}
|
||||
|
||||
export interface IAPIRefCheckRuns {
|
||||
readonly total_count: number
|
||||
readonly check_runs: IAPIRefCheckRun[]
|
||||
}
|
||||
|
||||
/** Protected branch information returned by the GitHub API */
|
||||
export interface IAPIPushControl {
|
||||
/**
|
||||
|
@ -767,18 +805,52 @@ export class API {
|
|||
|
||||
/**
|
||||
* Get the combined status for the given ref.
|
||||
*
|
||||
* Note: Contrary to many other methods in this class this will not
|
||||
* suppress or log errors, callers must ensure that they handle errors.
|
||||
*/
|
||||
public async fetchCombinedRefStatus(
|
||||
owner: string,
|
||||
name: string,
|
||||
ref: string
|
||||
): Promise<IAPIRefStatus> {
|
||||
const path = `repos/${owner}/${name}/commits/${ref}/status`
|
||||
): Promise<IAPIRefStatus | null> {
|
||||
const safeRef = encodeURIComponent(ref)
|
||||
const path = `repos/${owner}/${name}/commits/${safeRef}/status?per_page=100`
|
||||
const response = await this.request('GET', path)
|
||||
return await parsedResponse<IAPIRefStatus>(response)
|
||||
|
||||
try {
|
||||
return await parsedResponse<IAPIRefStatus>(response)
|
||||
} catch (err) {
|
||||
log.debug(
|
||||
`Failed fetching check runs for ref ${ref} (${owner}/${name})`,
|
||||
err
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any check run results for the given ref.
|
||||
*/
|
||||
public async fetchRefCheckRuns(
|
||||
owner: string,
|
||||
name: string,
|
||||
ref: string
|
||||
): Promise<IAPIRefCheckRuns | null> {
|
||||
const safeRef = encodeURIComponent(ref)
|
||||
const path = `repos/${owner}/${name}/commits/${safeRef}/check-runs?per_page=100`
|
||||
const headers = {
|
||||
Accept: 'application/vnd.github.antiope-preview+json',
|
||||
}
|
||||
|
||||
const response = await this.request('GET', path, undefined, headers)
|
||||
|
||||
try {
|
||||
return await parsedResponse<IAPIRefCheckRuns>(response)
|
||||
} catch (err) {
|
||||
log.debug(
|
||||
`Failed fetching check runs for ref ${ref} (${owner}/${name})`,
|
||||
err
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -133,3 +133,10 @@ export function enableForkSettings(): boolean {
|
|||
export function enableDiscardLines(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Should we allow to change the default branch when creating new repositories?
|
||||
*/
|
||||
export function enableDefaultBranchSetting(): boolean {
|
||||
return enableBetaFeatures()
|
||||
}
|
||||
|
|
|
@ -120,3 +120,20 @@ export async function checkoutPaths(
|
|||
'checkoutPaths'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and checkout the given branch.
|
||||
*
|
||||
* @param repository The repository.
|
||||
* @param branchName The branch to create and checkout.
|
||||
*/
|
||||
export async function createAndCheckoutBranch(
|
||||
repository: Repository,
|
||||
branchName: string
|
||||
): Promise<void> {
|
||||
await git(
|
||||
['checkout', '-b', branchName],
|
||||
repository.path,
|
||||
'createAndCheckoutBranch'
|
||||
)
|
||||
}
|
||||
|
|
|
@ -80,11 +80,9 @@ export async function resetSubmodulePaths(
|
|||
repository: Repository,
|
||||
paths: ReadonlyArray<string>
|
||||
): Promise<void> {
|
||||
for (const path of paths) {
|
||||
await git(
|
||||
['submodule', 'update', '--recursive', '--', path],
|
||||
repository.path,
|
||||
'updateSubmodule'
|
||||
)
|
||||
}
|
||||
await git(
|
||||
['submodule', 'update', '--recursive', '--force', '--', ...paths],
|
||||
repository.path,
|
||||
'updateSubmodule'
|
||||
)
|
||||
}
|
||||
|
|
39
app/src/lib/helpers/default-branch.ts
Normal file
39
app/src/lib/helpers/default-branch.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { getGlobalConfigValue, setGlobalConfigValue } from '../git'
|
||||
import { enableDefaultBranchSetting } from '../feature-flag'
|
||||
|
||||
export const DefaultBranchInGit = 'master'
|
||||
|
||||
const DefaultBranchSettingName = 'init.defaultBranch'
|
||||
|
||||
/**
|
||||
* The branch names that Desktop shows by default as radio buttons on the
|
||||
* form that allows users to change default branch name.
|
||||
*/
|
||||
export const SuggestedBranchNames: ReadonlyArray<string> = ['master', 'main']
|
||||
|
||||
/**
|
||||
* Returns the configured default branch when creating new repositories
|
||||
*/
|
||||
async function getConfiguredDefaultBranch(): Promise<string | null> {
|
||||
if (!enableDefaultBranchSetting()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return getGlobalConfigValue(DefaultBranchSettingName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured default branch when creating new repositories
|
||||
*/
|
||||
export async function getDefaultBranch(): Promise<string> {
|
||||
return (await getConfiguredDefaultBranch()) ?? DefaultBranchInGit
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the configured default branch when creating new repositories.
|
||||
*
|
||||
* @param branchName The default branch name to use.
|
||||
*/
|
||||
export async function setDefaultBranch(branchName: string) {
|
||||
return setGlobalConfigValue('init.defaultBranch', branchName)
|
||||
}
|
|
@ -4,15 +4,47 @@ import QuickLRU from 'quick-lru'
|
|||
import { Account } from '../../models/account'
|
||||
import { AccountsStore } from './accounts-store'
|
||||
import { GitHubRepository } from '../../models/github-repository'
|
||||
import { API, IAPIRefStatus } from '../api'
|
||||
import {
|
||||
API,
|
||||
IAPIRefStatusItem,
|
||||
IAPIRefCheckRun,
|
||||
APICheckStatus,
|
||||
APICheckConclusion,
|
||||
} from '../api'
|
||||
import { IDisposable, Disposable } from 'event-kit'
|
||||
|
||||
/**
|
||||
* A Desktop-specific model closely related to a GitHub API Check Run.
|
||||
*
|
||||
* The RefCheck object abstracts the difference between the legacy
|
||||
* Commit Status objects and the modern Check Runs and unifies them
|
||||
* under one common interface. Since all commit statuses can be
|
||||
* represented as Check Runs but not all Check Runs can be represented
|
||||
* as statuses the model closely aligns with Check Runs.
|
||||
*/
|
||||
export interface IRefCheck {
|
||||
readonly name: string
|
||||
readonly description: string
|
||||
readonly status: APICheckStatus
|
||||
readonly conclusion: APICheckConclusion | null
|
||||
}
|
||||
|
||||
/**
|
||||
* A combined view of all legacy commit statuses as well as
|
||||
* check runs for a particular Git reference.
|
||||
*/
|
||||
export interface ICombinedRefCheck {
|
||||
readonly status: APICheckStatus
|
||||
readonly conclusion: APICheckConclusion | null
|
||||
readonly checks: ReadonlyArray<IRefCheck>
|
||||
}
|
||||
|
||||
interface ICommitStatusCacheEntry {
|
||||
/**
|
||||
* The combined ref status from the API or null if
|
||||
* the status could not be retrieved.
|
||||
*/
|
||||
readonly status: IAPIRefStatus | null
|
||||
readonly check: ICombinedRefCheck | null
|
||||
|
||||
/**
|
||||
* The timestamp for when this cache entry was last
|
||||
|
@ -21,7 +53,7 @@ interface ICommitStatusCacheEntry {
|
|||
readonly fetchedAt: Date
|
||||
}
|
||||
|
||||
export type StatusCallBack = (status: IAPIRefStatus | null) => void
|
||||
export type StatusCallBack = (status: ICombinedRefCheck | null) => void
|
||||
|
||||
/**
|
||||
* An interface describing one or more subscriptions for
|
||||
|
@ -67,10 +99,6 @@ function getCacheKeyForRepository(repository: GitHubRepository, ref: string) {
|
|||
/**
|
||||
* Creates a cache key for a particular ref in a specific repository.
|
||||
*
|
||||
* Remarks: The cache key is currently the same as the canonical API status
|
||||
* URI but that has no bearing on the functionality, it does, however
|
||||
* help with debugging.
|
||||
*
|
||||
* @param endpoint The repository endpoint (for example https://api.github.com for
|
||||
* GitHub.com and https://github.corporation.local/api for GHE)
|
||||
* @param owner The repository owner's login (i.e niik for niik/desktop)
|
||||
|
@ -84,7 +112,7 @@ function getCacheKey(
|
|||
name: string,
|
||||
ref: string
|
||||
) {
|
||||
return `${endpoint}/repos/${owner}/${name}/commits/${ref}/status`
|
||||
return `${endpoint}/repos/${owner}/${name}/commits/${ref}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,7 +138,7 @@ function entryIsEligibleForRefresh(entry: ICommitStatusCacheEntry) {
|
|||
* application is focused.
|
||||
*/
|
||||
const BackgroundRefreshInterval = 3 * 60 * 1000
|
||||
const MaxConcurrentFetches = 5
|
||||
const MaxConcurrentFetches = 6
|
||||
|
||||
export class CommitStatusStore {
|
||||
/** The list of signed-in accounts, kept in sync with the accounts store */
|
||||
|
@ -246,13 +274,16 @@ export class CommitStatusStore {
|
|||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const api = API.fromAccount(account)
|
||||
const status = await api.fetchCombinedRefStatus(owner, name, ref)
|
||||
const api = API.fromAccount(account)
|
||||
|
||||
this.cache.set(key, { status, fetchedAt: new Date() })
|
||||
subscription.callbacks.forEach(cb => cb(status))
|
||||
} catch (err) {
|
||||
const [statuses, checkRuns] = await Promise.all([
|
||||
api.fetchCombinedRefStatus(owner, name, ref),
|
||||
api.fetchRefCheckRuns(owner, name, ref),
|
||||
])
|
||||
|
||||
const checks = new Array<IRefCheck>()
|
||||
|
||||
if (statuses === null && checkRuns === null) {
|
||||
// Okay, so we failed retrieving the status for one reason or another.
|
||||
// That's a bummer, but we still need to put something in the cache
|
||||
// or else we'll consider this subscription eligible for refresh
|
||||
|
@ -261,13 +292,27 @@ export class CommitStatusStore {
|
|||
// notifying subscribers we ensure they keep their current status
|
||||
// if they have one and that we attempt to fetch it again on the same
|
||||
// schedule as the others.
|
||||
log.debug(`Failed fetching status for ref ${ref} (${owner}/${name})`, err)
|
||||
|
||||
const existingEntry = this.cache.get(key)
|
||||
const status = existingEntry === undefined ? null : existingEntry.status
|
||||
const check = existingEntry?.check ?? null
|
||||
|
||||
this.cache.set(key, { status, fetchedAt: new Date() })
|
||||
this.cache.set(key, { check, fetchedAt: new Date() })
|
||||
return
|
||||
}
|
||||
|
||||
if (statuses !== null) {
|
||||
checks.push(...statuses.statuses.map(apiStatusToRefCheck))
|
||||
}
|
||||
|
||||
if (checkRuns !== null) {
|
||||
const latestCheckRunsByName = getLatestCheckRunsByName(
|
||||
checkRuns.check_runs
|
||||
)
|
||||
checks.push(...latestCheckRunsByName.map(apiCheckRunToRefCheck))
|
||||
}
|
||||
|
||||
const check = createCombinedCheckFromChecks(checks)
|
||||
this.cache.set(key, { check, fetchedAt: new Date() })
|
||||
subscription.callbacks.forEach(cb => cb(check))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -280,9 +325,9 @@ export class CommitStatusStore {
|
|||
public tryGetStatus(
|
||||
repository: GitHubRepository,
|
||||
ref: string
|
||||
): IAPIRefStatus | null {
|
||||
const entry = this.cache.get(getCacheKeyForRepository(repository, ref))
|
||||
return entry !== undefined ? entry.status : null
|
||||
): ICombinedRefCheck | null {
|
||||
const key = getCacheKeyForRepository(repository, ref)
|
||||
return this.cache.get(key)?.check ?? null
|
||||
}
|
||||
|
||||
private getOrCreateSubscription(repository: GitHubRepository, ref: string) {
|
||||
|
@ -334,3 +379,150 @@ export class CommitStatusStore {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a legacy API commit status to a fake check run
|
||||
*/
|
||||
function apiStatusToRefCheck(apiStatus: IAPIRefStatusItem): IRefCheck {
|
||||
let state: APICheckStatus
|
||||
let conclusion: APICheckConclusion | null = null
|
||||
|
||||
if (apiStatus.state === 'success') {
|
||||
state = 'completed'
|
||||
conclusion = 'success'
|
||||
} else if (apiStatus.state === 'pending') {
|
||||
state = 'in_progress'
|
||||
} else {
|
||||
state = 'completed'
|
||||
conclusion = 'failure'
|
||||
}
|
||||
|
||||
return {
|
||||
name: apiStatus.context,
|
||||
description: apiStatus.description,
|
||||
status: state,
|
||||
conclusion,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an API check run object to a RefCheck model
|
||||
*/
|
||||
function apiCheckRunToRefCheck(checkRun: IAPIRefCheckRun): IRefCheck {
|
||||
return {
|
||||
name: checkRun.name,
|
||||
description:
|
||||
checkRun?.output.title ?? checkRun.conclusion ?? checkRun.status,
|
||||
status: checkRun.status,
|
||||
conclusion: checkRun.conclusion,
|
||||
}
|
||||
}
|
||||
|
||||
function createCombinedCheckFromChecks(
|
||||
checks: ReadonlyArray<IRefCheck>
|
||||
): ICombinedRefCheck | null {
|
||||
if (checks.length === 0) {
|
||||
// This case is distinct from when we fail to call the API in
|
||||
// that this means there are no checks or statuses so we should
|
||||
// clear whatever info we've got for this ref.
|
||||
return null
|
||||
}
|
||||
|
||||
if (checks.length === 1) {
|
||||
// If we've got exactly one check then we can mirror its status
|
||||
// and conclusion 1-1 without having to create an aggregate status
|
||||
const { status, conclusion } = checks[0]
|
||||
return { status, conclusion, checks }
|
||||
}
|
||||
|
||||
if (checks.some(isIncompleteOrFailure)) {
|
||||
return { status: 'completed', conclusion: 'failure', checks }
|
||||
} else if (checks.every(isSuccess)) {
|
||||
return { status: 'completed', conclusion: 'success', checks }
|
||||
} else {
|
||||
return { status: 'in_progress', conclusion: null, checks }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the check is either incomplete or has failed
|
||||
*/
|
||||
export function isIncompleteOrFailure(check: IRefCheck) {
|
||||
return isIncomplete(check) || isFailure(check)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the check is incomplete (timed out, stale or cancelled).
|
||||
*
|
||||
* The terminology here is confusing and deserves explanation. An
|
||||
* incomplete check is a check run that has been started and who's
|
||||
* state is 'completed' but it never got to produce a conclusion
|
||||
* because it was either cancelled, it timed out, or GitHub marked
|
||||
* it as stale.
|
||||
*/
|
||||
export function isIncomplete(check: IRefCheck) {
|
||||
if (check.status === 'completed') {
|
||||
switch (check.conclusion) {
|
||||
case 'timed_out':
|
||||
case 'stale':
|
||||
case 'cancelled':
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/** Whether the check has failed (failure or requires action) */
|
||||
export function isFailure(check: IRefCheck) {
|
||||
if (check.status === 'completed') {
|
||||
switch (check.conclusion) {
|
||||
case 'failure':
|
||||
case 'action_required':
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/** Whether the check can be considered successful (success, neutral or skipped) */
|
||||
export function isSuccess(check: IRefCheck) {
|
||||
if (check.status === 'completed') {
|
||||
switch (check.conclusion) {
|
||||
case 'success':
|
||||
case 'neutral':
|
||||
case 'skipped':
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* In some cases there may be multiple check runs reported for a
|
||||
* reference. In that case GitHub.com will pick only the latest
|
||||
* run for each check name to present in the PR merge footer and
|
||||
* only the latest run counts towards the mergeability of a PR.
|
||||
*
|
||||
* We use the check suite id as a proxy for determining what's
|
||||
* the "latest" of two check runs with the same name.
|
||||
*/
|
||||
function getLatestCheckRunsByName(
|
||||
checkRuns: ReadonlyArray<IAPIRefCheckRun>
|
||||
): ReadonlyArray<IAPIRefCheckRun> {
|
||||
const latestCheckRunsByName = new Map<string, IAPIRefCheckRun>()
|
||||
|
||||
for (const checkRun of checkRuns) {
|
||||
const current = latestCheckRunsByName.get(checkRun.name)
|
||||
if (
|
||||
current === undefined ||
|
||||
current.check_suite.id < checkRun.check_suite.id
|
||||
) {
|
||||
latestCheckRunsByName.set(checkRun.name, checkRun)
|
||||
}
|
||||
}
|
||||
|
||||
return [...latestCheckRunsByName.values()]
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ import { PullRequest } from '../../models/pull-request'
|
|||
import { StatsStore } from '../stats'
|
||||
import { getTagsToPush, storeTagsToPush } from './helpers/tags-to-push-storage'
|
||||
import { DiffSelection, ITextDiff } from '../../models/diff'
|
||||
import { getDefaultBranch } from '../helpers/default-branch'
|
||||
|
||||
/** The number of commits to load from history per batch. */
|
||||
const CommitBatchSize = 100
|
||||
|
@ -524,7 +525,7 @@ export class GitStore extends BaseStore {
|
|||
}
|
||||
}
|
||||
|
||||
return 'master'
|
||||
return getDefaultBranch()
|
||||
}
|
||||
|
||||
private refreshRecentBranches(
|
||||
|
|
|
@ -43,6 +43,7 @@ export class AppWindow {
|
|||
disableBlinkFeatures: 'Auxclick',
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
spellcheck: false,
|
||||
},
|
||||
acceptFirstMouse: true,
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ export class CrashWindow {
|
|||
// See https://developers.google.com/web/updates/2016/10/auxclick
|
||||
disableBlinkFeatures: 'Auxclick',
|
||||
nodeIntegration: true,
|
||||
spellcheck: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { APIRefState } from '../lib/api'
|
||||
import { GitHubRepository } from './github-repository'
|
||||
|
||||
export class PullRequestRef {
|
||||
|
@ -15,13 +14,6 @@ export class PullRequestRef {
|
|||
) {}
|
||||
}
|
||||
|
||||
/** The commit status and metadata for a given ref */
|
||||
export interface ICommitStatus {
|
||||
readonly id: number
|
||||
readonly state: APIRefState
|
||||
readonly description: string
|
||||
}
|
||||
|
||||
export class PullRequest {
|
||||
/**
|
||||
* @param created The date on which the PR was created.
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
getStatus,
|
||||
getAuthorIdentity,
|
||||
isGitRepository,
|
||||
createAndCheckoutBranch,
|
||||
} from '../../lib/git'
|
||||
import { sanitizedRepositoryName } from './sanitized-repository-name'
|
||||
import { TextBox } from '../lib/text-box'
|
||||
|
@ -30,6 +31,10 @@ import { PopupType } from '../../models/popup'
|
|||
import { Ref } from '../lib/ref'
|
||||
import { enableReadmeOverwriteWarning } from '../../lib/feature-flag'
|
||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import {
|
||||
getDefaultBranch,
|
||||
DefaultBranchInGit,
|
||||
} from '../../lib/helpers/default-branch'
|
||||
|
||||
/** The sentinel value used to indicate no gitignore should be used. */
|
||||
const NoGitIgnoreValue = 'None'
|
||||
|
@ -240,6 +245,25 @@ export class CreateRepository extends React.Component<
|
|||
|
||||
const repository = repositories[0]
|
||||
|
||||
const defaultBranch = await getDefaultBranch()
|
||||
|
||||
if (defaultBranch !== DefaultBranchInGit) {
|
||||
try {
|
||||
// Manually checkout to the configured default branch.
|
||||
// TODO (git@2.28): Remove this code when upgrading to git v2.28
|
||||
// since this will be natively implemented.
|
||||
await createAndCheckoutBranch(repository, defaultBranch)
|
||||
} catch (e) {
|
||||
// When we cannot checkout the default branch just log the error,
|
||||
// since we don't want to stop the repository creation (since we're
|
||||
// in the middle of the creation process).
|
||||
log.error(
|
||||
`createRepository: unable to create default branch "${defaultBranch}"`,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.createWithReadme) {
|
||||
try {
|
||||
await writeDefaultReadme(
|
||||
|
|
|
@ -6,13 +6,10 @@ import {
|
|||
DialogFooter,
|
||||
DefaultDialogFooter,
|
||||
} from './dialog'
|
||||
import {
|
||||
dialogTransitionEnterTimeout,
|
||||
dialogTransitionLeaveTimeout,
|
||||
} from './app'
|
||||
import { dialogTransitionTimeout } from './app'
|
||||
import { GitError, isAuthFailureError } from '../lib/git/core'
|
||||
import { Popup, PopupType } from '../models/popup'
|
||||
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import { OkCancelButtonGroup } from './dialog/ok-cancel-button-group'
|
||||
import { ErrorWithMetadata } from '../lib/error-with-metadata'
|
||||
import { RetryActionType, RetryAction } from '../models/retry-actions'
|
||||
|
@ -82,7 +79,7 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
|||
// with the next error in the queue.
|
||||
window.setTimeout(() => {
|
||||
this.props.onClearError(currentError)
|
||||
}, dialogTransitionLeaveTimeout)
|
||||
}, dialogTransitionTimeout.exit)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,7 +90,7 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
|||
//being open at the same time.
|
||||
window.setTimeout(() => {
|
||||
this.props.onShowPopup({ type: PopupType.Preferences })
|
||||
}, dialogTransitionLeaveTimeout)
|
||||
}, dialogTransitionTimeout.exit)
|
||||
}
|
||||
|
||||
private onRetryAction = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
|
@ -262,15 +259,16 @@ export class AppError extends React.Component<IAppErrorProps, IAppErrorState> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const dialogContent = this.renderDialog()
|
||||
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
transitionName="modal"
|
||||
component="div"
|
||||
transitionEnterTimeout={dialogTransitionEnterTimeout}
|
||||
transitionLeaveTimeout={dialogTransitionLeaveTimeout}
|
||||
>
|
||||
{this.renderDialog()}
|
||||
</CSSTransitionGroup>
|
||||
<TransitionGroup>
|
||||
{dialogContent && (
|
||||
<CSSTransition classNames="modal" timeout={dialogTransitionTimeout}>
|
||||
{dialogContent}
|
||||
</CSSTransition>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { ipcRenderer, remote } from 'electron'
|
||||
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
|
||||
import {
|
||||
IAppState,
|
||||
|
@ -154,8 +154,12 @@ interface IAppProps {
|
|||
readonly startTime: number
|
||||
}
|
||||
|
||||
export const dialogTransitionEnterTimeout = 250
|
||||
export const dialogTransitionLeaveTimeout = 100
|
||||
export const dialogTransitionTimeout = {
|
||||
enter: 250,
|
||||
exit: 100,
|
||||
}
|
||||
|
||||
export const bannerTransitionTimeout = { enter: 500, exit: 400 }
|
||||
|
||||
/**
|
||||
* The time to delay (in ms) from when we've loaded the initial state to showing
|
||||
|
@ -2142,15 +2146,16 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
}
|
||||
|
||||
private renderPopup() {
|
||||
const popupContent = this.currentPopupContent()
|
||||
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
transitionName="modal"
|
||||
component="div"
|
||||
transitionEnterTimeout={dialogTransitionEnterTimeout}
|
||||
transitionLeaveTimeout={dialogTransitionLeaveTimeout}
|
||||
>
|
||||
{this.currentPopupContent()}
|
||||
</CSSTransitionGroup>
|
||||
<TransitionGroup>
|
||||
{popupContent && (
|
||||
<CSSTransition classNames="modal" timeout={dialogTransitionTimeout}>
|
||||
{popupContent}
|
||||
</CSSTransition>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2505,14 +2510,13 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
banner = this.renderUpdateBanner()
|
||||
}
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
transitionName="banner"
|
||||
component="div"
|
||||
transitionEnterTimeout={500}
|
||||
transitionLeaveTimeout={400}
|
||||
>
|
||||
{banner}
|
||||
</CSSTransitionGroup>
|
||||
<TransitionGroup>
|
||||
{banner && (
|
||||
<CSSTransition classNames="banner" timeout={bannerTransitionTimeout}>
|
||||
{banner}
|
||||
</CSSTransition>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import * as React from 'react'
|
||||
import { Octicon, OcticonSymbol } from '../octicons'
|
||||
import { APIRefState, IAPIRefStatus } from '../../lib/api'
|
||||
import { assertNever } from '../../lib/fatal-error'
|
||||
import classNames from 'classnames'
|
||||
import { getRefStatusSummary } from './pull-request-status'
|
||||
import { GitHubRepository } from '../../models/github-repository'
|
||||
import { IDisposable } from 'event-kit'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import {
|
||||
ICombinedRefCheck,
|
||||
isSuccess,
|
||||
} from '../../lib/stores/commit-status-store'
|
||||
|
||||
interface ICIStatusProps {
|
||||
/** The classname for the underlying element. */
|
||||
|
@ -22,7 +23,7 @@ interface ICIStatusProps {
|
|||
}
|
||||
|
||||
interface ICIStatusState {
|
||||
readonly status: IAPIRefStatus | null
|
||||
readonly check: ICombinedRefCheck | null
|
||||
}
|
||||
|
||||
/** The little CI status indicator. */
|
||||
|
@ -35,7 +36,7 @@ export class CIStatus extends React.PureComponent<
|
|||
public constructor(props: ICIStatusProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
status: props.dispatcher.tryGetCommitStatus(
|
||||
check: props.dispatcher.tryGetCommitStatus(
|
||||
this.props.repository,
|
||||
this.props.commitRef
|
||||
),
|
||||
|
@ -66,7 +67,7 @@ export class CIStatus extends React.PureComponent<
|
|||
this.props.commitRef !== prevProps.commitRef
|
||||
) {
|
||||
this.setState({
|
||||
status: this.props.dispatcher.tryGetCommitStatus(
|
||||
check: this.props.dispatcher.tryGetCommitStatus(
|
||||
this.props.repository,
|
||||
this.props.commitRef
|
||||
),
|
||||
|
@ -83,43 +84,87 @@ export class CIStatus extends React.PureComponent<
|
|||
this.unsubscribe()
|
||||
}
|
||||
|
||||
private onStatus = (status: IAPIRefStatus | null) => {
|
||||
this.setState({ status })
|
||||
private onStatus = (check: ICombinedRefCheck | null) => {
|
||||
this.setState({ check })
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { status } = this.state
|
||||
const { check } = this.state
|
||||
|
||||
if (status === null || status.total_count === 0) {
|
||||
if (check === null || check.checks.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const title = getRefStatusSummary(status)
|
||||
const state = status.state
|
||||
|
||||
return (
|
||||
<Octicon
|
||||
className={classNames(
|
||||
'ci-status',
|
||||
`ci-status-${state}`,
|
||||
`ci-status-${getClassNameForCheck(check)}`,
|
||||
this.props.className
|
||||
)}
|
||||
symbol={getSymbolForState(state)}
|
||||
title={title}
|
||||
symbol={getSymbolForCheck(check)}
|
||||
title={getRefCheckSummary(check)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getSymbolForState(state: APIRefState): OcticonSymbol {
|
||||
switch (state) {
|
||||
case 'pending':
|
||||
return OcticonSymbol.dotFill
|
||||
function getSymbolForCheck(check: ICombinedRefCheck): OcticonSymbol {
|
||||
switch (check.conclusion) {
|
||||
case 'timed_out':
|
||||
return OcticonSymbol.x
|
||||
case 'failure':
|
||||
return OcticonSymbol.x
|
||||
case 'neutral':
|
||||
return OcticonSymbol.squareFill
|
||||
case 'success':
|
||||
return OcticonSymbol.check
|
||||
default:
|
||||
return assertNever(state, `Unknown state: ${state}`)
|
||||
case 'cancelled':
|
||||
return OcticonSymbol.stop
|
||||
case 'action_required':
|
||||
return OcticonSymbol.alert
|
||||
case 'skipped':
|
||||
return OcticonSymbol.skip
|
||||
case 'stale':
|
||||
return OcticonSymbol.issueReopened
|
||||
}
|
||||
|
||||
// Pending
|
||||
return OcticonSymbol.dotFill
|
||||
}
|
||||
|
||||
function getClassNameForCheck(check: ICombinedRefCheck): string {
|
||||
switch (check.conclusion) {
|
||||
case 'timed_out':
|
||||
return 'timed-out'
|
||||
case 'action_required':
|
||||
return 'action-required'
|
||||
case 'failure':
|
||||
case 'neutral':
|
||||
case 'success':
|
||||
case 'cancelled':
|
||||
case 'skipped':
|
||||
case 'stale':
|
||||
return check.conclusion
|
||||
}
|
||||
|
||||
// Pending
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the combined check to an app-friendly string.
|
||||
*/
|
||||
export function getRefCheckSummary(check: ICombinedRefCheck): string {
|
||||
if (check.checks.length === 1) {
|
||||
const { name, description } = check.checks[0]
|
||||
return `${name}: ${description}`
|
||||
}
|
||||
|
||||
const successCount = check.checks.reduce(
|
||||
(acc, cur) => acc + (isSuccess(cur) ? 1 : 0),
|
||||
0
|
||||
)
|
||||
|
||||
return `${successCount}/${check.checks.length} checks OK`
|
||||
}
|
||||
|
|
|
@ -79,11 +79,13 @@ export class PullRequestListItem extends React.Component<
|
|||
private renderPullRequestStatus() {
|
||||
const ref = `refs/pull/${this.props.number}/head`
|
||||
return (
|
||||
<CIStatus
|
||||
dispatcher={this.props.dispatcher}
|
||||
repository={this.props.repository}
|
||||
commitRef={ref}
|
||||
/>
|
||||
<div className="ci-status-container">
|
||||
<CIStatus
|
||||
dispatcher={this.props.dispatcher}
|
||||
repository={this.props.repository}
|
||||
commitRef={ref}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import { ICommitStatus } from '../../models/pull-request'
|
||||
import { APIRefState, IAPIRefStatus } from '../../lib/api'
|
||||
import { assertNever } from '../../lib/fatal-error'
|
||||
|
||||
function formatState(state: APIRefState): string {
|
||||
switch (state) {
|
||||
case 'failure':
|
||||
return 'Commit status: failed'
|
||||
case 'pending':
|
||||
case 'success':
|
||||
return `Commit status: ${state}`
|
||||
|
||||
default:
|
||||
return assertNever(state, `Unknown APIRefState value: ${state}`)
|
||||
}
|
||||
}
|
||||
|
||||
function formatSingleStatus(status: ICommitStatus) {
|
||||
const word = status.state
|
||||
const sentenceCaseWord =
|
||||
word.charAt(0).toUpperCase() + word.substring(1, word.length)
|
||||
|
||||
return `${sentenceCaseWord}: ${status.description}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the Pull Request status to an app-friendly string.
|
||||
*
|
||||
* If the pull request contains commit statuses, this method will compute
|
||||
* the number of successful statuses. Oteherwise, it will fall back
|
||||
* to the `state` value reported by the GitHub API.
|
||||
*/
|
||||
export function getRefStatusSummary(prStatus: IAPIRefStatus): string {
|
||||
const statusCount = prStatus.statuses.length || 0
|
||||
|
||||
if (statusCount === 0) {
|
||||
return formatState(prStatus.state)
|
||||
}
|
||||
|
||||
if (statusCount === 1) {
|
||||
return formatSingleStatus(prStatus.statuses[0])
|
||||
}
|
||||
|
||||
const successCount = prStatus.statuses.filter(x => x.state === 'success')
|
||||
.length
|
||||
|
||||
return `${successCount}/${statusCount} checks OK`
|
||||
}
|
|
@ -27,7 +27,7 @@ import {
|
|||
} from '../autocompletion'
|
||||
import { ClickSource } from '../lib/list'
|
||||
import { WorkingDirectoryFileChange } from '../../models/status'
|
||||
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import { openFile } from '../lib/open-file'
|
||||
import { Account } from '../../models/account'
|
||||
import { PopupType } from '../../models/popup'
|
||||
|
@ -329,27 +329,23 @@ export class ChangesSidebar extends React.Component<IChangesSidebarProps, {}> {
|
|||
// the commit won't be completely deleted because the tag will still point to it.
|
||||
if (commit && commit.tags.length === 0) {
|
||||
child = (
|
||||
<UndoCommit
|
||||
isPushPullFetchInProgress={this.props.isPushPullFetchInProgress}
|
||||
commit={commit}
|
||||
onUndo={this.onUndo}
|
||||
emoji={this.props.emoji}
|
||||
isCommitting={this.props.isCommitting}
|
||||
/>
|
||||
<CSSTransition
|
||||
classNames="undo"
|
||||
appear={true}
|
||||
timeout={UndoCommitAnimationTimeout}
|
||||
>
|
||||
<UndoCommit
|
||||
isPushPullFetchInProgress={this.props.isPushPullFetchInProgress}
|
||||
commit={commit}
|
||||
onUndo={this.onUndo}
|
||||
emoji={this.props.emoji}
|
||||
isCommitting={this.props.isCommitting}
|
||||
/>
|
||||
</CSSTransition>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
transitionName="undo"
|
||||
transitionAppear={true}
|
||||
transitionAppearTimeout={UndoCommitAnimationTimeout}
|
||||
transitionEnterTimeout={UndoCommitAnimationTimeout}
|
||||
transitionLeaveTimeout={UndoCommitAnimationTimeout}
|
||||
>
|
||||
{child}
|
||||
</CSSTransitionGroup>
|
||||
)
|
||||
return <TransitionGroup>{child}</TransitionGroup>
|
||||
}
|
||||
|
||||
private renderUndoCommit = (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { remote } from 'electron'
|
||||
import { Disposable, IDisposable } from 'event-kit'
|
||||
|
||||
import { IAPIOrganization, IAPIRefStatus, IAPIRepository } from '../../lib/api'
|
||||
import { IAPIOrganization, IAPIRepository } from '../../lib/api'
|
||||
import { shell } from '../../lib/app-shell'
|
||||
import {
|
||||
CompareAction,
|
||||
|
@ -86,6 +86,7 @@ import { executeMenuItem } from '../main-process-proxy'
|
|||
import {
|
||||
CommitStatusStore,
|
||||
StatusCallBack,
|
||||
ICombinedRefCheck,
|
||||
} from '../../lib/stores/commit-status-store'
|
||||
import { MergeResult } from '../../models/merge'
|
||||
import {
|
||||
|
@ -2340,7 +2341,7 @@ export class Dispatcher {
|
|||
public tryGetCommitStatus(
|
||||
repository: GitHubRepository,
|
||||
ref: string
|
||||
): IAPIRefStatus | null {
|
||||
): ICombinedRefCheck | null {
|
||||
return this.commitStatusStore.tryGetStatus(repository, ref)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
|
||||
import { Commit } from '../../models/commit'
|
||||
import {
|
||||
|
@ -32,6 +32,8 @@ import { MergeCallToActionWithConflicts } from './merge-call-to-action-with-conf
|
|||
import { assertNever } from '../../lib/fatal-error'
|
||||
import { enableNDDBBanner } from '../../lib/feature-flag'
|
||||
|
||||
const DivergingBannerAnimationTimeout = 300
|
||||
|
||||
interface ICompareSidebarProps {
|
||||
readonly repository: Repository
|
||||
readonly isLocalRepository: boolean
|
||||
|
@ -153,20 +155,11 @@ export class CompareSidebar extends React.Component<
|
|||
public render() {
|
||||
const { allBranches, filterText, showBranchList } = this.props.compareState
|
||||
const placeholderText = getPlaceholderText(this.props.compareState)
|
||||
const DivergingBannerAnimationTimeout = 300
|
||||
|
||||
return (
|
||||
<div id="compare-view">
|
||||
{enableNDDBBanner() && (
|
||||
<CSSTransitionGroup
|
||||
transitionName="diverge-banner"
|
||||
transitionAppear={true}
|
||||
transitionAppearTimeout={DivergingBannerAnimationTimeout}
|
||||
transitionEnterTimeout={DivergingBannerAnimationTimeout}
|
||||
transitionLeaveTimeout={DivergingBannerAnimationTimeout}
|
||||
>
|
||||
{this.renderNotificationBanner()}
|
||||
</CSSTransitionGroup>
|
||||
<TransitionGroup>{this.renderNotificationBanner()}</TransitionGroup>
|
||||
)}
|
||||
|
||||
<div className="compare-form">
|
||||
|
@ -205,15 +198,23 @@ export class CompareSidebar extends React.Component<
|
|||
return inferredComparisonBranch.branch !== null &&
|
||||
inferredComparisonBranch.aheadBehind !== null &&
|
||||
inferredComparisonBranch.aheadBehind.behind > 0 ? (
|
||||
<div className="diverge-banner-wrapper">
|
||||
<NewCommitsBanner
|
||||
dispatcher={this.props.dispatcher}
|
||||
repository={this.props.repository}
|
||||
commitsBehindBaseBranch={inferredComparisonBranch.aheadBehind.behind}
|
||||
baseBranch={inferredComparisonBranch.branch}
|
||||
onDismiss={this.onNotificationBannerDismissed}
|
||||
/>
|
||||
</div>
|
||||
<CSSTransition
|
||||
classNames="diverge-banner"
|
||||
appear={true}
|
||||
timeout={DivergingBannerAnimationTimeout}
|
||||
>
|
||||
<div className="diverge-banner-wrapper">
|
||||
<NewCommitsBanner
|
||||
dispatcher={this.props.dispatcher}
|
||||
repository={this.props.repository}
|
||||
commitsBehindBaseBranch={
|
||||
inferredComparisonBranch.aheadBehind.behind
|
||||
}
|
||||
baseBranch={inferredComparisonBranch.branch}
|
||||
onDismiss={this.onNotificationBannerDismissed}
|
||||
/>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
) : null
|
||||
}
|
||||
|
||||
|
|
|
@ -92,6 +92,12 @@ enableSourceMaps()
|
|||
// see https://github.com/desktop/dugite/pull/85
|
||||
process.env['LOCAL_GIT_DIRECTORY'] = Path.resolve(__dirname, 'git')
|
||||
|
||||
// Ensure that dugite infers the GIT_EXEC_PATH
|
||||
// based on the LOCAL_GIT_DIRECTORY env variable
|
||||
// instead of just blindly trusting what's set in
|
||||
// the current environment. See https://git.io/JJ7KF
|
||||
delete process.env.GIT_EXEC_PATH
|
||||
|
||||
momentDurationFormatSetup(moment)
|
||||
|
||||
const startTime = performance.now()
|
||||
|
|
|
@ -54,6 +54,8 @@ export class RefNameTextBox extends React.Component<
|
|||
IRefNameProps,
|
||||
IRefNameState
|
||||
> {
|
||||
private textBoxRef = React.createRef<TextBox>()
|
||||
|
||||
public constructor(props: IRefNameProps) {
|
||||
super(props)
|
||||
|
||||
|
@ -80,6 +82,7 @@ export class RefNameTextBox extends React.Component<
|
|||
<TextBox
|
||||
label={this.props.label}
|
||||
value={this.state.proposedValue}
|
||||
ref={this.textBoxRef}
|
||||
onValueChanged={this.onValueChange}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
|
@ -89,6 +92,16 @@ export class RefNameTextBox extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically moves keyboard focus to the inner text input element if it can be focused
|
||||
* (i.e. if it's not disabled explicitly or implicitly through for example a fieldset).
|
||||
*/
|
||||
public focus() {
|
||||
if (this.textBoxRef.current !== null) {
|
||||
this.textBoxRef.current.focus()
|
||||
}
|
||||
}
|
||||
|
||||
private onValueChange = (proposedValue: string) => {
|
||||
const sanitizedValue = sanitizedRefName(proposedValue)
|
||||
const previousSanitizedValue = this.state.sanitizedValue
|
||||
|
|
|
@ -2,16 +2,60 @@ import * as React from 'react'
|
|||
import { TextBox } from '../lib/text-box'
|
||||
import { Row } from '../lib/row'
|
||||
import { DialogContent } from '../dialog'
|
||||
import { SuggestedBranchNames } from '../../lib/helpers/default-branch'
|
||||
import { RefNameTextBox } from '../lib/ref-name-text-box'
|
||||
import { Ref } from '../lib/ref'
|
||||
import { RadioButton } from '../lib/radio-button'
|
||||
import { enableDefaultBranchSetting } from '../../lib/feature-flag'
|
||||
|
||||
interface IGitProps {
|
||||
readonly name: string
|
||||
readonly email: string
|
||||
readonly defaultBranch: string
|
||||
|
||||
readonly onNameChanged: (name: string) => void
|
||||
readonly onEmailChanged: (email: string) => void
|
||||
readonly onDefaultBranchChanged: (defaultBranch: string) => void
|
||||
}
|
||||
|
||||
export class Git extends React.Component<IGitProps, {}> {
|
||||
interface IGitState {
|
||||
/**
|
||||
* True if the default branch setting is not one of the suggestions.
|
||||
* It's used to display the "Other" text box that allows the user to
|
||||
* enter a custom branch name.
|
||||
*/
|
||||
readonly defaultBranchIsOther: boolean
|
||||
}
|
||||
|
||||
// This will be the prepopulated branch name on the "other" input
|
||||
// field when the user selects it.
|
||||
const OtherNameForDefaultBranch = ''
|
||||
|
||||
export class Git extends React.Component<IGitProps, IGitState> {
|
||||
private defaultBranchInputRef = React.createRef<RefNameTextBox>()
|
||||
|
||||
public constructor(props: IGitProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
defaultBranchIsOther: !SuggestedBranchNames.includes(
|
||||
this.props.defaultBranch
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IGitProps) {
|
||||
// Focus the text input that allows the user to enter a custom
|
||||
// branch name when the user has selected "Other...".
|
||||
if (
|
||||
this.props.defaultBranch !== prevProps.defaultBranch &&
|
||||
this.props.defaultBranch === OtherNameForDefaultBranch &&
|
||||
this.defaultBranchInputRef.current !== null
|
||||
) {
|
||||
this.defaultBranchInputRef.current.focus()
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<DialogContent>
|
||||
|
@ -29,7 +73,92 @@ export class Git extends React.Component<IGitProps, {}> {
|
|||
onValueChanged={this.props.onEmailChanged}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
{this.renderDefaultBranchSetting()}
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
private renderWarningMessage = (
|
||||
sanitizedBranchName: string,
|
||||
proposedBranchName: string
|
||||
) => {
|
||||
if (sanitizedBranchName === '') {
|
||||
return (
|
||||
<>
|
||||
<Ref>{proposedBranchName}</Ref> is an invalid branch name.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
Will be saved as <Ref>{sanitizedBranchName}</Ref>.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private renderDefaultBranchSetting() {
|
||||
if (!enableDefaultBranchSetting()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { defaultBranchIsOther } = this.state
|
||||
|
||||
return (
|
||||
<div className="default-branch-component">
|
||||
<h2>Default branch for new repositories</h2>
|
||||
|
||||
{SuggestedBranchNames.map((branchName: string) => (
|
||||
<RadioButton
|
||||
key={branchName}
|
||||
checked={
|
||||
!defaultBranchIsOther && this.props.defaultBranch === branchName
|
||||
}
|
||||
value={branchName}
|
||||
label={branchName}
|
||||
onSelected={this.onDefaultBranchChanged}
|
||||
/>
|
||||
))}
|
||||
<RadioButton
|
||||
key={OtherNameForDefaultBranch}
|
||||
checked={defaultBranchIsOther}
|
||||
value={OtherNameForDefaultBranch}
|
||||
label="Other…"
|
||||
onSelected={this.onDefaultBranchChanged}
|
||||
/>
|
||||
|
||||
{defaultBranchIsOther && (
|
||||
<RefNameTextBox
|
||||
initialValue={this.props.defaultBranch}
|
||||
renderWarningMessage={this.renderWarningMessage}
|
||||
onValueChange={this.props.onDefaultBranchChanged}
|
||||
ref={this.defaultBranchInputRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
<p className="git-settings-description">
|
||||
These preferences will edit your global Git config.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler to make sure that we show/hide the text box to enter a custom
|
||||
* branch name when the user clicks on one of the radio buttons.
|
||||
*
|
||||
* We don't want to call this handler on changes to the text box since that
|
||||
* will cause the text box to be hidden if the user types a branch name
|
||||
* that starts with one of the suggested branch names (e.g `mastera`).
|
||||
*
|
||||
* @param defaultBranch string the selected default branch
|
||||
*/
|
||||
private onDefaultBranchChanged = (defaultBranch: string) => {
|
||||
this.setState({
|
||||
defaultBranchIsOther: !SuggestedBranchNames.includes(defaultBranch),
|
||||
})
|
||||
|
||||
this.props.onDefaultBranchChanged(defaultBranch)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@ import {
|
|||
parseConfigLockFilePathFromError,
|
||||
} from '../../lib/git'
|
||||
import { ConfigLockFileExists } from '../lib/config-lock-file-exists'
|
||||
import {
|
||||
setDefaultBranch,
|
||||
getDefaultBranch,
|
||||
} from '../../lib/helpers/default-branch'
|
||||
|
||||
interface IPreferencesProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
|
@ -54,8 +58,10 @@ interface IPreferencesState {
|
|||
readonly selectedIndex: PreferencesTab
|
||||
readonly committerName: string
|
||||
readonly committerEmail: string
|
||||
readonly defaultBranch: string
|
||||
readonly initialCommitterName: string | null
|
||||
readonly initialCommitterEmail: string | null
|
||||
readonly initialDefaultBranch: string | null
|
||||
readonly disallowedCharactersMessage: string | null
|
||||
readonly optOutOfUsageTracking: boolean
|
||||
readonly confirmRepositoryRemoval: boolean
|
||||
|
@ -90,8 +96,10 @@ export class Preferences extends React.Component<
|
|||
selectedIndex: this.props.initialSelectedTab || PreferencesTab.Accounts,
|
||||
committerName: '',
|
||||
committerEmail: '',
|
||||
defaultBranch: '',
|
||||
initialCommitterName: null,
|
||||
initialCommitterEmail: null,
|
||||
initialDefaultBranch: null,
|
||||
disallowedCharactersMessage: null,
|
||||
availableEditors: [],
|
||||
optOutOfUsageTracking: false,
|
||||
|
@ -110,6 +118,7 @@ export class Preferences extends React.Component<
|
|||
public async componentWillMount() {
|
||||
const initialCommitterName = await getGlobalConfigValue('user.name')
|
||||
const initialCommitterEmail = await getGlobalConfigValue('user.email')
|
||||
const initialDefaultBranch = await getDefaultBranch()
|
||||
|
||||
// There's no point in us reading http.schannelCheckRevoke on macOS, it's
|
||||
// just a wasted Git process since the option only affects Windows. Besides,
|
||||
|
@ -150,8 +159,10 @@ export class Preferences extends React.Component<
|
|||
this.setState({
|
||||
committerName,
|
||||
committerEmail,
|
||||
defaultBranch: initialDefaultBranch,
|
||||
initialCommitterName,
|
||||
initialCommitterEmail,
|
||||
initialDefaultBranch,
|
||||
optOutOfUsageTracking: this.props.optOutOfUsageTracking,
|
||||
confirmRepositoryRemoval: this.props.confirmRepositoryRemoval,
|
||||
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
||||
|
@ -278,8 +289,10 @@ export class Preferences extends React.Component<
|
|||
<Git
|
||||
name={this.state.committerName}
|
||||
email={this.state.committerEmail}
|
||||
defaultBranch={this.state.defaultBranch}
|
||||
onNameChanged={this.onCommitterNameChanged}
|
||||
onEmailChanged={this.onCommitterEmailChanged}
|
||||
onDefaultBranchChanged={this.onDefaultBranchChanged}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
@ -372,6 +385,10 @@ export class Preferences extends React.Component<
|
|||
this.setState({ committerEmail })
|
||||
}
|
||||
|
||||
private onDefaultBranchChanged = (defaultBranch: string) => {
|
||||
this.setState({ defaultBranch })
|
||||
}
|
||||
|
||||
private onSelectedEditorChanged = (editor: ExternalEditor) => {
|
||||
this.setState({ selectedExternalEditor: editor })
|
||||
}
|
||||
|
@ -431,6 +448,20 @@ export class Preferences extends React.Component<
|
|||
await setGlobalConfigValue('user.email', this.state.committerEmail)
|
||||
}
|
||||
|
||||
// If the entered default branch is empty, we don't store it and keep
|
||||
// the previous value.
|
||||
// We do this because the preferences dialog doesn't have error states,
|
||||
// and since the preferences dialog have a global "Save" button (that will
|
||||
// save all the changes performed in every single tab), we cannot
|
||||
// block the user from clicking "Save" because the entered branch is not valid
|
||||
// (they will not be able to know the issue if they are in a different tab).
|
||||
if (
|
||||
this.state.defaultBranch.length > 0 &&
|
||||
this.state.defaultBranch !== this.state.initialDefaultBranch
|
||||
) {
|
||||
await setDefaultBranch(this.state.defaultBranch)
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.schannelCheckRevoke !==
|
||||
this.state.initialSchannelCheckRevoke &&
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import { WindowState } from '../../lib/window-state'
|
||||
|
||||
interface IFullScreenInfoProps {
|
||||
|
@ -11,8 +11,7 @@ interface IFullScreenInfoState {
|
|||
readonly renderTransitionGroup: boolean
|
||||
}
|
||||
|
||||
const transitionAppearDuration = 100
|
||||
const transitionLeaveDuration = 250
|
||||
const toastTransitionTimeout = { appear: 100, exit: 250 }
|
||||
const holdDuration = 3000
|
||||
|
||||
/**
|
||||
|
@ -60,7 +59,9 @@ export class FullScreenInfo extends React.Component<
|
|||
|
||||
this.transitionGroupDisappearTimeoutId = window.setTimeout(
|
||||
this.onTransitionGroupDisappearTimeout,
|
||||
transitionAppearDuration + holdDuration + transitionLeaveDuration
|
||||
toastTransitionTimeout.appear +
|
||||
holdDuration +
|
||||
toastTransitionTimeout.exit
|
||||
)
|
||||
|
||||
this.setState({
|
||||
|
@ -93,9 +94,17 @@ export class FullScreenInfo extends React.Component<
|
|||
const kbdShortcut = __DARWIN__ ? '⌃⌘F' : 'F11'
|
||||
|
||||
return (
|
||||
<div key="notification" className="toast-notification">
|
||||
Press <kbd>{kbdShortcut}</kbd> to exit fullscreen
|
||||
</div>
|
||||
<CSSTransition
|
||||
classNames="toast-animation"
|
||||
appear={true}
|
||||
enter={false}
|
||||
exit={true}
|
||||
timeout={toastTransitionTimeout}
|
||||
>
|
||||
<div key="notification" className="toast-notification">
|
||||
Press <kbd>{kbdShortcut}</kbd> to exit fullscreen
|
||||
</div>
|
||||
</CSSTransition>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -105,18 +114,9 @@ export class FullScreenInfo extends React.Component<
|
|||
}
|
||||
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
className="toast-notification-container"
|
||||
transitionName="toast-animation"
|
||||
component="div"
|
||||
transitionAppear={true}
|
||||
transitionEnter={false}
|
||||
transitionLeave={true}
|
||||
transitionAppearTimeout={transitionAppearDuration}
|
||||
transitionLeaveTimeout={transitionLeaveDuration}
|
||||
>
|
||||
<TransitionGroup className="toast-notification-container">
|
||||
{this.renderFullScreenNotification()}
|
||||
</CSSTransitionGroup>
|
||||
</TransitionGroup>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
|
||||
interface IZoomInfoProps {
|
||||
readonly windowZoomFactor: number
|
||||
|
@ -91,9 +91,17 @@ export class ZoomInfo extends React.Component<IZoomInfoProps, IZoomInfoState> {
|
|||
const zoomPercent = `${(this.state.windowZoomFactor * 100).toFixed(0)}%`
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>{zoomPercent}</span>
|
||||
</div>
|
||||
<CSSTransition
|
||||
classNames={this.state.transitionName}
|
||||
appear={true}
|
||||
enter={false}
|
||||
exit={true}
|
||||
timeout={transitionDuration}
|
||||
>
|
||||
<div>
|
||||
<span>{zoomPercent}</span>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -103,18 +111,9 @@ export class ZoomInfo extends React.Component<IZoomInfoProps, IZoomInfoState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
id="window-zoom-info"
|
||||
transitionName={this.state.transitionName}
|
||||
component="div"
|
||||
transitionAppear={true}
|
||||
transitionEnter={false}
|
||||
transitionLeave={true}
|
||||
transitionAppearTimeout={transitionDuration}
|
||||
transitionLeaveTimeout={transitionDuration}
|
||||
>
|
||||
<TransitionGroup id="window-zoom-info">
|
||||
{this.renderZoomInfo()}
|
||||
</CSSTransitionGroup>
|
||||
</TransitionGroup>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,11 +61,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.banner-leave {
|
||||
&.banner-exit {
|
||||
height: $banner-height;
|
||||
opacity: 1;
|
||||
|
||||
&.banner-leave-active {
|
||||
&.banner-exit-active {
|
||||
height: 0px;
|
||||
opacity: 0;
|
||||
transition: height 225ms ease-in-out 175ms, opacity 175ms ease-in;
|
||||
|
|
|
@ -106,8 +106,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ci-status {
|
||||
.ci-status-container {
|
||||
margin-right: var(--spacing-half);
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,10 +7,19 @@
|
|||
color: $yellow-700;
|
||||
}
|
||||
|
||||
.ci-status-timed-out,
|
||||
.ci-status-action-required,
|
||||
.ci-status-failure {
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
.ci-status-cancelled,
|
||||
.ci-status-stale,
|
||||
.ci-status-skipped,
|
||||
.ci-status-neutral {
|
||||
color: $gray-400;
|
||||
}
|
||||
|
||||
.ci-status-success {
|
||||
color: $green-500;
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ dialog {
|
|||
}
|
||||
}
|
||||
|
||||
&-leave {
|
||||
&-exit {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
pointer-events: none;
|
||||
|
@ -80,7 +80,7 @@ dialog {
|
|||
}
|
||||
}
|
||||
|
||||
&-leave-active {
|
||||
&-exit-active {
|
||||
opacity: 0.01;
|
||||
transform: scale(0.25);
|
||||
transition: opacity 100ms ease-in, transform 100ms var(--easing-ease-in-back);
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&-leave {
|
||||
&-exit {
|
||||
max-height: 200px;
|
||||
|
||||
&-active {
|
||||
|
|
|
@ -59,6 +59,20 @@
|
|||
color: var(--text-secondary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.default-branch-component {
|
||||
margin-top: var(--spacing-double);
|
||||
|
||||
.ref-name-text-box {
|
||||
margin-top: var(--spacing);
|
||||
}
|
||||
}
|
||||
|
||||
.git-settings-description {
|
||||
margin-top: var(--spacing-double);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
}
|
||||
|
||||
#external-editor-error {
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
.radio-button-component {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& + .radio-button-component {
|
||||
margin-top: var(--spacing-half);
|
||||
}
|
||||
|
||||
label {
|
||||
& > input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > label {
|
||||
margin: 0;
|
||||
margin-left: var(--spacing-half);
|
||||
}
|
||||
|
|
|
@ -27,5 +27,24 @@
|
|||
input {
|
||||
@include textboxish;
|
||||
@include textboxish-disabled;
|
||||
|
||||
// Customize the "clear" icon on search text inputs to match our
|
||||
// theme icons and colors.
|
||||
//
|
||||
// To do so, we're creating an opaque 16x16 element with the background color
|
||||
// that we want the icon to appear in and then apply the icon path
|
||||
// as a mask, that way we can control the color dynamically based on
|
||||
// our variables instead of hardcoding it in the SVG.
|
||||
&[type='search']::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 0;
|
||||
background-color: var(--text-color);
|
||||
// The following SVG corresponds to the `x` octicon in 16px size:
|
||||
// https://github.com/primer/octicons/blob/4661c1e0aa30c7d252318d2b003af782a6891089/icons/x-16.svg#L1
|
||||
-webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/></svg>');
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,11 +117,11 @@
|
|||
transition: max-height var(--undo-animation-duration) ease-in;
|
||||
}
|
||||
|
||||
.undo-leave {
|
||||
.undo-exit {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.undo-leave.undo-leave-active {
|
||||
.undo-exit.undo-exit-active {
|
||||
max-height: 0;
|
||||
|
||||
transition: max-height var(--undo-animation-duration) ease-out;
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
transition: all 100ms ease-out;
|
||||
}
|
||||
|
||||
&-leave-active {
|
||||
&-exit-active {
|
||||
opacity: 0;
|
||||
transition: all 250ms ease-out;
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
.zoom-in,
|
||||
.zoom-out {
|
||||
&-leave-active {
|
||||
&-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 100ms ease-out;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as path from 'path'
|
||||
import { readFile, writeFile } from 'fs-extra'
|
||||
|
||||
import { Repository } from '../../../src/models/repository'
|
||||
import {
|
||||
|
@ -73,5 +74,20 @@ describe('git/submodule', () => {
|
|||
result = await listSubmodules(repository)
|
||||
expect(result[0].describe).toBe('first-tag~2')
|
||||
})
|
||||
|
||||
it('eliminate submodule dirty state', async () => {
|
||||
const testRepoPath = await setupFixtureRepository('submodule-basic-setup')
|
||||
const repository = new Repository(testRepoPath, -1, null, false)
|
||||
|
||||
const submodulePath = path.join(testRepoPath, 'foo', 'submodule')
|
||||
|
||||
const filePath = path.join(submodulePath, 'README.md')
|
||||
await writeFile(filePath, 'changed', { encoding: 'utf8' })
|
||||
|
||||
await resetSubmodulePaths(repository, ['foo/submodule'])
|
||||
|
||||
const result = await readFile(filePath, { encoding: 'utf8' })
|
||||
expect(result).toBe('# submodule-test-case')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
import { APIRefState } from '../../src/lib/api'
|
||||
import { getRefStatusSummary } from '../../src/ui/branches/pull-request-status'
|
||||
|
||||
const failure: APIRefState = 'failure'
|
||||
const pending: APIRefState = 'pending'
|
||||
const success: APIRefState = 'success'
|
||||
|
||||
describe('pull request status', () => {
|
||||
it('uses the state when no statuses found', () => {
|
||||
const status = {
|
||||
state: success,
|
||||
total_count: 0,
|
||||
statuses: [],
|
||||
}
|
||||
expect(getRefStatusSummary(status)).toBe('Commit status: success')
|
||||
})
|
||||
|
||||
it('changes the failure message to something more friendly', () => {
|
||||
const status = {
|
||||
state: failure,
|
||||
total_count: 0,
|
||||
statuses: [],
|
||||
}
|
||||
expect(getRefStatusSummary(status)).toBe('Commit status: failed')
|
||||
})
|
||||
|
||||
it('reads the statuses when they are populated', () => {
|
||||
const status = {
|
||||
state: success,
|
||||
total_count: 2,
|
||||
statuses: [
|
||||
{
|
||||
id: 1,
|
||||
state: success,
|
||||
description: 'first',
|
||||
target_url: '',
|
||||
context: '2',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
state: success,
|
||||
description: 'second',
|
||||
target_url: '',
|
||||
context: '2',
|
||||
},
|
||||
],
|
||||
}
|
||||
expect(getRefStatusSummary(status)).toBe('2/2 checks OK')
|
||||
})
|
||||
|
||||
it('a successful status shows the description', () => {
|
||||
const status = {
|
||||
state: success,
|
||||
total_count: 2,
|
||||
statuses: [
|
||||
{
|
||||
id: 1,
|
||||
state: success,
|
||||
description: 'The Travis CI build passed',
|
||||
target_url: '',
|
||||
context: '1',
|
||||
},
|
||||
],
|
||||
}
|
||||
expect(getRefStatusSummary(status)).toBe(
|
||||
'Success: The Travis CI build passed'
|
||||
)
|
||||
})
|
||||
|
||||
it('an error status shows the description', () => {
|
||||
const status = {
|
||||
state: success,
|
||||
total_count: 2,
|
||||
statuses: [
|
||||
{
|
||||
id: 1,
|
||||
state: failure,
|
||||
description: 'The Travis CI build failed',
|
||||
target_url: '',
|
||||
context: '1',
|
||||
},
|
||||
],
|
||||
}
|
||||
expect(getRefStatusSummary(status)).toBe(
|
||||
'Failure: The Travis CI build failed'
|
||||
)
|
||||
})
|
||||
|
||||
it('only counts the successful statuses', () => {
|
||||
const status = {
|
||||
state: success,
|
||||
total_count: 3,
|
||||
statuses: [
|
||||
{
|
||||
id: 1,
|
||||
state: success,
|
||||
description: 'first',
|
||||
target_url: '',
|
||||
context: '1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
state: pending,
|
||||
description: 'second',
|
||||
target_url: '',
|
||||
context: '2',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
state: pending,
|
||||
description: 'third',
|
||||
target_url: '',
|
||||
context: '3',
|
||||
},
|
||||
],
|
||||
}
|
||||
expect(getRefStatusSummary(status)).toBe('1/3 checks OK')
|
||||
})
|
||||
})
|
|
@ -59,7 +59,6 @@ const commonConfig: webpack.Configuration = {
|
|||
],
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
modules: [path.resolve(__dirname, 'node_modules/')],
|
||||
},
|
||||
node: {
|
||||
__dirname: false,
|
||||
|
|
|
@ -9,6 +9,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.12.0"
|
||||
|
||||
"@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
|
||||
version "7.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.0.tgz#f10245877042a815e07f7e693faff0ae9d3a2aac"
|
||||
integrity sha512-qArkXsjJq7H+T86WrIFV0Fnu/tNOkZ4cgXmjkzAu3b/58D5mFIO8JH/y77t7C9q0OdDRdh9s7Ue5GasYssxtXw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@sindresorhus/is@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||
|
@ -274,6 +281,11 @@ cross-spawn@^6.0.0:
|
|||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.2.tgz#ee5ff8f208c8cd613b389f7b222c9801ca62b3f7"
|
||||
integrity sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==
|
||||
|
||||
cycle@1.0.x:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
|
||||
|
@ -344,18 +356,21 @@ dom-classlist@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/dom-classlist/-/dom-classlist-1.0.1.tgz#722814dc2f509d3d7d06f2533ba9ff48eeea59b9"
|
||||
integrity sha1-cigU3C9QnT19BvJTO6n/SO7qWbk=
|
||||
|
||||
"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.2.0:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a"
|
||||
integrity sha1-MgPgf+0he9H0JLAZc1WC/Deyglo=
|
||||
|
||||
dom-helpers@^3.3.1:
|
||||
"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.3.1:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
|
||||
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.1.2"
|
||||
|
||||
dom-helpers@^5.0.1:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b"
|
||||
integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.7"
|
||||
csstype "^3.0.2"
|
||||
|
||||
dom-matches@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-matches/-/dom-matches-2.0.0.tgz#d2728b416a87533980eb089b848d253cf23a758c"
|
||||
|
@ -1213,7 +1228,7 @@ promise@^7.1.1:
|
|||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
prop-types@^15.5.6, prop-types@^15.6.0:
|
||||
prop-types@^15.6.0:
|
||||
version "15.6.0"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
|
||||
integrity sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=
|
||||
|
@ -1325,16 +1340,15 @@ react-lifecycles-compat@^3.0.4:
|
|||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
|
||||
react-transition-group@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6"
|
||||
integrity sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q==
|
||||
react-transition-group@^4.4.1:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
|
||||
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
|
||||
dependencies:
|
||||
chain-function "^1.0.0"
|
||||
dom-helpers "^3.2.0"
|
||||
loose-envify "^1.3.1"
|
||||
prop-types "^15.5.6"
|
||||
warning "^3.0.0"
|
||||
"@babel/runtime" "^7.5.5"
|
||||
dom-helpers "^5.0.1"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-virtualized@^9.20.0:
|
||||
version "9.20.0"
|
||||
|
@ -1403,6 +1417,11 @@ regenerator-runtime@^0.12.0:
|
|||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
|
||||
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
|
||||
|
||||
regenerator-runtime@^0.13.4:
|
||||
version "0.13.7"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
||||
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
|
||||
|
||||
registry-js@^1.4.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/registry-js/-/registry-js-1.6.0.tgz#b7414b29690fb9287a2ea2d65980be0810303e49"
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
{
|
||||
"releases": {
|
||||
"2.5.4-beta4": [
|
||||
"[Added] Support for specifying the default branch for new repositories - #10262",
|
||||
"[Improved] Enable discarding submodule working directory changes - #8218",
|
||||
"[Improved] Fix label alignment in radio-button component styles - #10397",
|
||||
"[Improved] Custom icon in list filter text boxes - #10394",
|
||||
"[Removed] Disable spellchecking for the time being - #10398"
|
||||
],
|
||||
"2.5.4-beta3": [
|
||||
"[Added] Add Alacritty shell support - #10243. Thanks @halitogunc!"
|
||||
],
|
||||
"2.5.4-beta2": [
|
||||
"[Added] Suggest to stash changes when trying to do an operation that requires a clean working directory - #10053",
|
||||
"[Fixed] Pull request list is now keyboard accessible - #10334",
|
||||
|
|
|
@ -4,30 +4,28 @@
|
|||
|
||||
To authenticate against Azure DevOps repositories you will need to create a personal access token.
|
||||
|
||||
1. Go to your Azure DevOps account and select **Security** in the user profile dropdown:
|
||||
1. Go to your Azure DevOps account and select **Personal Access Tokens** in the user settings dropdown:
|
||||
|
||||
![](https://user-images.githubusercontent.com/4404199/29400833-79755fe0-8337-11e7-8cfb-1d346a6801b4.png)
|
||||
![](https://user-images.githubusercontent.com/792378/90431645-f9d9cd80-e08e-11ea-9fb4-ca8ba2a5d769.png)
|
||||
|
||||
2. Select **Personal access tokens**
|
||||
2. Click **New token** to create a new personal access token. Give it a name, select the organizations you would like the token to apply to, and choose when you would like the token to expire.
|
||||
|
||||
3. Click **New token** to create a new personal access token. Give it a name, select the organizations you would like the token to apply to, and choose when you would like the token to expire.
|
||||
- **Note:** For the **Expiration** dropdown you can select **Custom defined** to select an expiration date up to a year in advance of the current date. This is useful if you do not want to have to periodically go back and generate a new token after your current token expires.
|
||||
|
||||
- **Note:** For the **Expiration** dropdown you can select **Custom defined** to select an expiration date up to a year in advance of the current date. This is useful if you do not want to have to periodically go back and generate a new token after your current token expires.
|
||||
3 . Under the **Scopes** section choose **Custom defined** and then select **Read & Write** under the **Code** section. This will grant GitHub Desktop read and write access to your Azure DevOps repositories.
|
||||
|
||||
4. Under the **Scopes** section choose **Custom defined** and then select **Read & Write** under the **Code** section. This will grant GitHub Desktop read and write access to your Azure DevOps repositories.
|
||||
4 . Click **Create** to create a new token, and then copy it to your clipboard.
|
||||
|
||||
5. Click **Create** to create a new token, and then copy it to your clipboard.
|
||||
|
||||
![](https://user-images.githubusercontent.com/721500/51131191-fd470c00-17fc-11e9-8895-94f3784ebd4b.png)
|
||||
|
||||
## Cloning your Azure DevOps repository in GitHub Desktop
|
||||
|
||||
1. Open GitHub Desktop and go to **File** > **Clone Repository** > **URL**. Enter the Git URL of your Azure DevOps repository. Make sure you enter the correct URL, which should have the following structure:
|
||||
|
||||
|
||||
`https://<username>@dev.azure.com/<username>/<project_name>/_git/<repository_name>`
|
||||
|
||||
|
||||
2. You will receive an `Authentication Failed` error. Enter your Azure DevOps username and paste in the token you just copied to your clipboard. Click **Save and Retry** to successfully clone the repository to your local machine in GitHub Desktop.
|
||||
|
||||
|
||||
![](https://user-images.githubusercontent.com/4404199/29401109-8bf03536-8338-11e7-8abb-b467378b6115.png)
|
||||
|
||||
- **Note:** Your Azure DevOps credentials will be securely stored on your local machine so you will not need to repeat this process when cloning another repository from Azure DevOps.
|
||||
|
|
|
@ -6,9 +6,9 @@ We introduced syntax highlighted diffs in [#3101](https://github.com/desktop/des
|
|||
|
||||
## Supported languages
|
||||
|
||||
We currently support syntax highlighting for the following languages.
|
||||
We currently support syntax highlighting for the following languages and file types.
|
||||
|
||||
JavaScript, JSON, TypeScript, Coffeescript, HTML, CSS, SCSS, LESS, VUE, Markdown, Yaml, XML, Objective-C, Scala, C#, Java, C, C++, Kotlin, Ocaml, F#, sh/bash, Swift, SQL, CYPHER, Go, Perl, PHP, Python, Ruby, Clojure, Rust, Elixir, Haxe, R, JavaServer Pages, PowerShell, Docker, Visual Basic, Fortran, Lua, Crystal, Julia, sTex, SPARQL, Stylus, Soy, Smalltalk, Slim, Sieve, Scheme, ReStructuredText, RPM, Q, Puppet, Pug, Protobuf, Properties, Apache Pig, ASCII Armor (PGP), Oz and Pascal.
|
||||
JavaScript, JSON, TypeScript, Coffeescript, HTML, Asp, JavaServer Pages, CSS, SCSS, LESS, VUE, Markdown, Yaml, XML, Diff, Objective-C, Scala, C#, Java, C, C++, Kotlin, Ocaml, F#, Swift, sh/bash, SQL, CYPHER, Go, Perl, PHP, Python, Ruby, Clojure, Rust, Elixir, Haxe, R, PowerShell, Visual Basic, Fortran, Lua, Crystal, Julia, sTex, SPARQL, Stylus, Soy, Smalltalk, Slim, Sieve, Scheme, ReStructuredText, RPM, Q, Puppet, Pug, Protobuf, Properties, Apache Pig, ASCII Armor (PGP), Oz, Pascal and Docker.
|
||||
|
||||
This list was never meant to be exhaustive, we expect to add more languages going forward but this seemed like a good first step.
|
||||
|
||||
|
|
|
@ -149,7 +149,7 @@
|
|||
"@types/react": "^16.8.7",
|
||||
"@types/react-css-transition-replace": "^2.1.3",
|
||||
"@types/react-dom": "^16.8.2",
|
||||
"@types/react-transition-group": "1.1.1",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/react-virtualized": "^9.7.12",
|
||||
"@types/request": "^2.0.9",
|
||||
"@types/semver": "^5.5.0",
|
||||
|
|
|
@ -36,10 +36,10 @@ function packageOSX() {
|
|||
const dest = getOSXZipPath()
|
||||
fs.removeSync(dest)
|
||||
|
||||
console.log('Packaging for macOS…')
|
||||
cp.execSync(
|
||||
`ditto -ck --keepParent "${distPath}/${productName}.app" "${dest}"`
|
||||
)
|
||||
console.log(`Zipped to ${dest}`)
|
||||
}
|
||||
|
||||
function packageWindows() {
|
||||
|
@ -53,7 +53,8 @@ function packageWindows() {
|
|||
)
|
||||
|
||||
if (isAppveyor() || isGitHubActions()) {
|
||||
cp.execSync(`powershell ${setupCertificatePath}`)
|
||||
console.log('Installing signing certificate…')
|
||||
cp.execSync(`powershell ${setupCertificatePath}`, { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
const iconSource = path.join(
|
||||
|
@ -108,6 +109,7 @@ function packageWindows() {
|
|||
options.signWithParams = `/f ${certificatePath} /p ${process.env.WINDOWS_CERT_PASSWORD} /tr http://timestamp.digicert.com /td sha256 /fd sha256`
|
||||
}
|
||||
|
||||
console.log('Packaging for Windows…')
|
||||
electronInstaller
|
||||
.createWindowsInstaller(options)
|
||||
.then(() => {
|
||||
|
@ -141,5 +143,6 @@ function packageLinux() {
|
|||
configPath,
|
||||
]
|
||||
|
||||
console.log('Packaging for Linux…')
|
||||
cp.spawnSync(electronBuilder, args, { stdio: 'inherit' })
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import * as Crypto from 'crypto'
|
|||
import request from 'request'
|
||||
|
||||
console.log('Packaging…')
|
||||
execSync('yarn package')
|
||||
execSync('yarn package', { stdio: 'inherit' })
|
||||
|
||||
const sha = platforms.getSha().substr(0, 8)
|
||||
|
||||
|
|
21
yarn.lock
21
yarn.lock
|
@ -607,10 +607,10 @@
|
|||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-1.1.1.tgz#372fd2b4777b96aa983ac15fb5cc0ce150550aeb"
|
||||
integrity sha512-LOXbB5NTmyaZeiysrSRhgQDGFic7gyavZ6HcBkWwSL7PW+aYRh/cOPO+wLp2YnHJLWTQz9Avr3qOXWCrWhGOfA==
|
||||
"@types/react-transition-group@^4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d"
|
||||
integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
|
@ -6729,15 +6729,10 @@ kind-of@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
|
||||
integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
|
||||
|
||||
kind-of@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.0.tgz#3606e9e2fa960e7ddaa8898c03804e47e5d66644"
|
||||
integrity sha512-sUd5AnFyOPh+RW+ZIHd1FHuwM4OFvhKCPVxxhamLxWLpmv1xQ394lzRMmhLQOiMpXvnB64YRLezWaJi5xGk7Dg==
|
||||
|
||||
kind-of@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
|
||||
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
|
||||
kind-of@^6.0.0, kind-of@^6.0.2:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
||||
|
||||
klaw-sync@^3.0.0:
|
||||
version "3.0.2"
|
||||
|
|
Loading…
Reference in a new issue