mirror of
https://github.com/desktop/desktop
synced 2024-09-19 16:12:20 +00:00
Merge branch 'development' into rebase-multi-comit-op
This commit is contained in:
commit
f607b4a40f
|
@ -25,6 +25,7 @@ rules:
|
|||
react-no-unbound-dispatcher-props: error
|
||||
react-readonly-props-and-state: error
|
||||
react-proper-lifecycle-methods: error
|
||||
set-almost-immediate: error
|
||||
|
||||
###########
|
||||
# PLUGINS #
|
||||
|
|
|
@ -13,7 +13,6 @@ module.exports = {
|
|||
'!**/vendor/**',
|
||||
'!**/*.d.*',
|
||||
// not focused on testing these areas currently
|
||||
'!src/ask-pass/**/*',
|
||||
'!src/cli/**/*',
|
||||
'!src/crash/**/*',
|
||||
'!src/highlighter/**/*',
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"productName": "GitHub Desktop",
|
||||
"bundleID": "com.github.GitHubClient",
|
||||
"companyName": "GitHub, Inc.",
|
||||
"version": "2.9.1-beta2",
|
||||
"version": "2.9.1-beta7",
|
||||
"main": "./main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -25,7 +25,7 @@
|
|||
"codemirror-mode-elixir": "^1.1.2",
|
||||
"compare-versions": "^3.6.0",
|
||||
"deep-equal": "^1.0.1",
|
||||
"desktop-trampoline": "desktop/desktop-trampoline#v0.9.7",
|
||||
"desktop-trampoline": "desktop/desktop-trampoline#v0.9.8",
|
||||
"detect-arm64-translation": "https://github.com/desktop/node-detect-arm64-translation#v1.0.4",
|
||||
"dexie": "^2.0.0",
|
||||
"double-ended-queue": "^2.1.0-0",
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { getKeyForEndpoint } from '../lib/auth'
|
||||
import { TokenStore } from '../lib/stores/token-store'
|
||||
|
||||
/** Parse the GIT_ASKPASS prompt and determine the appropriate response. */
|
||||
export async function responseForPrompt(
|
||||
prompt: string
|
||||
): Promise<string | null> {
|
||||
const username = process.env.DESKTOP_USERNAME
|
||||
if (username == null || username.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (prompt.startsWith('Username')) {
|
||||
return username
|
||||
} else if (prompt.startsWith('Password')) {
|
||||
const endpoint = process.env.DESKTOP_ENDPOINT
|
||||
if (endpoint == null || endpoint.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const key = getKeyForEndpoint(endpoint)
|
||||
return await TokenStore.getItem(key, username)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/**
|
||||
* This will be run by the `ask-pass-trampoline`.
|
||||
*/
|
||||
|
||||
import { responseForPrompt } from './ask-pass'
|
||||
|
||||
const prompt = process.argv[2]
|
||||
responseForPrompt(prompt).then(response => {
|
||||
if (response) {
|
||||
process.stdout.write(response)
|
||||
process.stdout.end()
|
||||
}
|
||||
})
|
|
@ -21,6 +21,8 @@ type ExpectedInstallationChecker = (
|
|||
publisher: string
|
||||
) => boolean
|
||||
|
||||
type RegistryKey = { key: HKEY; subKey: string }
|
||||
|
||||
/** Represents an external editor on Windows */
|
||||
interface IWindowsExternalEditor {
|
||||
/** Name of the editor. It will be used both as identifier and user-facing. */
|
||||
|
@ -32,13 +34,13 @@ interface IWindowsExternalEditor {
|
|||
* Some tools (like VSCode) may support a 64-bit or 32-bit version of the
|
||||
* tool - we should use whichever they have installed.
|
||||
*/
|
||||
readonly registryKeys: ReadonlyArray<{ key: HKEY; subKey: string }>
|
||||
readonly registryKeys: ReadonlyArray<RegistryKey>
|
||||
|
||||
/**
|
||||
* List of path components from the editor's installation folder to the
|
||||
* executable shim.
|
||||
* List of lists of path components from the editor's installation folder to
|
||||
* the potential executable shims.
|
||||
**/
|
||||
readonly executableShimPath: ReadonlyArray<string>
|
||||
readonly executableShimPaths: ReadonlyArray<ReadonlyArray<string>>
|
||||
|
||||
/**
|
||||
* Registry key with the install location of the app. If not provided,
|
||||
|
@ -56,7 +58,7 @@ interface IWindowsExternalEditor {
|
|||
readonly expectedInstallationChecker: ExpectedInstallationChecker
|
||||
}
|
||||
|
||||
const registryKey = (key: HKEY, ...subKeys: string[]) => ({
|
||||
const registryKey = (key: HKEY, ...subKeys: string[]): RegistryKey => ({
|
||||
key,
|
||||
subKey: Path.win32.join(...subKeys),
|
||||
})
|
||||
|
@ -76,6 +78,39 @@ const LocalMachineUninstallKey = (subKey: string) =>
|
|||
const Wow64LocalMachineUninstallKey = (subKey: string) =>
|
||||
registryKey(HKEY.HKEY_LOCAL_MACHINE, wow64UninstallSubKey, subKey)
|
||||
|
||||
// This function generates registry keys for a given JetBrains product for the
|
||||
// last 2 years, assuming JetBrains makes no more than 5 releases per year.
|
||||
const registryKeysForJetBrainsIDE = (
|
||||
product: string
|
||||
): ReadonlyArray<RegistryKey> => {
|
||||
const maxReleasesPerYear = 5
|
||||
const lastYear = new Date().getFullYear()
|
||||
const firstYear = lastYear - 2
|
||||
|
||||
const result = new Array<RegistryKey>()
|
||||
|
||||
for (let year = firstYear; year <= lastYear; year++) {
|
||||
for (let release = 1; release <= maxReleasesPerYear; release++) {
|
||||
const key = `${product} ${year}.${release}`
|
||||
result.push(Wow64LocalMachineUninstallKey(key))
|
||||
result.push(CurrentUserUninstallKey(key))
|
||||
}
|
||||
}
|
||||
|
||||
// Return in reverse order to prioritize newer versions
|
||||
return result.reverse()
|
||||
}
|
||||
|
||||
// JetBrains IDEs might have 64 and/or 32 bit executables, so let's add both.
|
||||
const executableShimPathsForJetBrainsIDE = (
|
||||
baseName: string
|
||||
): ReadonlyArray<ReadonlyArray<string>> => {
|
||||
return [
|
||||
['bin', `${baseName}64.exe`],
|
||||
['bin', `${baseName}.exe`],
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* This list contains all the external editors supported on Windows. Add a new
|
||||
* entry here to add support for your favorite editor.
|
||||
|
@ -84,21 +119,21 @@ const editors: IWindowsExternalEditor[] = [
|
|||
{
|
||||
name: 'Atom',
|
||||
registryKeys: [CurrentUserUninstallKey('atom')],
|
||||
executableShimPath: ['bin', 'atom.cmd'],
|
||||
executableShimPaths: [['bin', 'atom.cmd']],
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName === 'Atom' && publisher === 'GitHub Inc.',
|
||||
},
|
||||
{
|
||||
name: 'Atom Beta',
|
||||
registryKeys: [CurrentUserUninstallKey('atom-beta')],
|
||||
executableShimPath: ['bin', 'atom-beta.cmd'],
|
||||
executableShimPaths: [['bin', 'atom-beta.cmd']],
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName === 'Atom Beta' && publisher === 'GitHub Inc.',
|
||||
},
|
||||
{
|
||||
name: 'Atom Nightly',
|
||||
registryKeys: [CurrentUserUninstallKey('atom-nightly')],
|
||||
executableShimPath: ['bin', 'atom-nightly.cmd'],
|
||||
executableShimPaths: [['bin', 'atom-nightly.cmd']],
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName === 'Atom Nightly' && publisher === 'GitHub Inc.',
|
||||
},
|
||||
|
@ -120,7 +155,7 @@ const editors: IWindowsExternalEditor[] = [
|
|||
// ARM64 version of VSCode (system)
|
||||
LocalMachineUninstallKey('{A5270FC5-65AD-483E-AC30-2C276B63D0AC}_is1'),
|
||||
],
|
||||
executableShimPath: ['bin', 'code.cmd'],
|
||||
executableShimPaths: [['bin', 'code.cmd']],
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('Microsoft Visual Studio Code') &&
|
||||
publisher === 'Microsoft Corporation',
|
||||
|
@ -143,7 +178,7 @@ const editors: IWindowsExternalEditor[] = [
|
|||
// ARM64 version of VSCode (system)
|
||||
LocalMachineUninstallKey('{0AEDB616-9614-463B-97D7-119DD86CCA64}_is1'),
|
||||
],
|
||||
executableShimPath: ['bin', 'code-insiders.cmd'],
|
||||
executableShimPaths: [['bin', 'code-insiders.cmd']],
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('Microsoft Visual Studio Code Insiders') &&
|
||||
publisher === 'Microsoft Corporation',
|
||||
|
@ -166,7 +201,7 @@ const editors: IWindowsExternalEditor[] = [
|
|||
// ARM64 version of VSCodium (system)
|
||||
LocalMachineUninstallKey('{D1ACE434-89C5-48D1-88D3-E2991DF85475}_is1'),
|
||||
],
|
||||
executableShimPath: ['bin', 'codium.cmd'],
|
||||
executableShimPaths: [['bin', 'codium.cmd']],
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('VSCodium') &&
|
||||
publisher === 'Microsoft Corporation',
|
||||
|
@ -179,7 +214,7 @@ const editors: IWindowsExternalEditor[] = [
|
|||
// Sublime Text 3
|
||||
LocalMachineUninstallKey('Sublime Text 3_is1'),
|
||||
],
|
||||
executableShimPath: ['subl.exe'],
|
||||
executableShimPaths: [['subl.exe']],
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('Sublime Text') &&
|
||||
publisher === 'Sublime HQ Pty Ltd',
|
||||
|
@ -192,7 +227,7 @@ const editors: IWindowsExternalEditor[] = [
|
|||
// 64-bit version of ColdFusionBuilder2016
|
||||
LocalMachineUninstallKey('Adobe ColdFusion Builder 2016'),
|
||||
],
|
||||
executableShimPath: ['CFBuilder.exe'],
|
||||
executableShimPaths: [['CFBuilder.exe']],
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('Adobe ColdFusion Builder') &&
|
||||
publisher === 'Adobe Systems Incorporated',
|
||||
|
@ -207,7 +242,7 @@ const editors: IWindowsExternalEditor[] = [
|
|||
'{37771A20-7167-44C0-B322-FD3E54C56156}_is1'
|
||||
),
|
||||
],
|
||||
executableShimPath: ['typora.exe'],
|
||||
executableShimPaths: [['typora.exe']],
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('Typora') && publisher === 'typora.io',
|
||||
},
|
||||
|
@ -237,32 +272,21 @@ const editors: IWindowsExternalEditor[] = [
|
|||
// 64-bit version of SlickEdit Pro 2014 (19.0.2)
|
||||
LocalMachineUninstallKey('{7CC0E567-ACD6-41E8-95DA-154CEEDB0A18}'),
|
||||
],
|
||||
executableShimPath: ['win', 'vs.exe'],
|
||||
executableShimPaths: [['win', 'vs.exe']],
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('SlickEdit') && publisher === 'SlickEdit Inc.',
|
||||
},
|
||||
{
|
||||
name: 'JetBrains Webstorm',
|
||||
registryKeys: [
|
||||
Wow64LocalMachineUninstallKey('WebStorm 2018.3'),
|
||||
Wow64LocalMachineUninstallKey('WebStorm 2019.2'),
|
||||
Wow64LocalMachineUninstallKey('WebStorm 2019.2.4'),
|
||||
Wow64LocalMachineUninstallKey('WebStorm 2019.3'),
|
||||
Wow64LocalMachineUninstallKey('WebStorm 2020.1'),
|
||||
],
|
||||
executableShimPath: ['bin', 'webstorm.exe'],
|
||||
registryKeys: registryKeysForJetBrainsIDE('WebStorm'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('webstorm'),
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('WebStorm') && publisher === 'JetBrains s.r.o.',
|
||||
},
|
||||
{
|
||||
name: 'JetBrains Phpstorm',
|
||||
registryKeys: [
|
||||
Wow64LocalMachineUninstallKey('PhpStorm 2019.2'),
|
||||
Wow64LocalMachineUninstallKey('PhpStorm 2019.2.4'),
|
||||
Wow64LocalMachineUninstallKey('PhpStorm 2019.3'),
|
||||
Wow64LocalMachineUninstallKey('PhpStorm 2020.1'),
|
||||
],
|
||||
executableShimPath: ['bin', 'phpstorm.exe'],
|
||||
registryKeys: registryKeysForJetBrainsIDE('PhpStorm'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('phpstorm'),
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('PhpStorm') && publisher === 'JetBrains s.r.o.',
|
||||
},
|
||||
|
@ -274,15 +298,15 @@ const editors: IWindowsExternalEditor[] = [
|
|||
// 32-bit version of Notepad++
|
||||
Wow64LocalMachineUninstallKey('Notepad++'),
|
||||
],
|
||||
executableShimPath: [],
|
||||
executableShimPaths: [],
|
||||
installLocationRegistryKey: 'DisplayIcon',
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('Notepad++') && publisher === 'Notepad++ Team',
|
||||
},
|
||||
{
|
||||
name: 'JetBrains Rider',
|
||||
registryKeys: [Wow64LocalMachineUninstallKey('JetBrains Rider 2019.3.4')],
|
||||
executableShimPath: ['bin', 'rider64.exe'],
|
||||
registryKeys: registryKeysForJetBrainsIDE('JetBrains Rider'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('rider'),
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('JetBrains Rider') &&
|
||||
publisher === 'JetBrains s.r.o.',
|
||||
|
@ -290,11 +314,29 @@ const editors: IWindowsExternalEditor[] = [
|
|||
{
|
||||
name: 'RStudio',
|
||||
registryKeys: [Wow64LocalMachineUninstallKey('RStudio')],
|
||||
executableShimPath: [],
|
||||
executableShimPaths: [],
|
||||
installLocationRegistryKey: 'DisplayIcon',
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName === 'RStudio' && publisher === 'RStudio',
|
||||
},
|
||||
{
|
||||
name: 'JetBrains IntelliJ Idea',
|
||||
registryKeys: registryKeysForJetBrainsIDE('IntelliJ IDEA'),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('idea'),
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('IntelliJ IDEA ') &&
|
||||
publisher === 'JetBrains s.r.o.',
|
||||
},
|
||||
{
|
||||
name: 'JetBrains IntelliJ Idea Community Edition',
|
||||
registryKeys: registryKeysForJetBrainsIDE(
|
||||
'IntelliJ IDEA Community Edition'
|
||||
),
|
||||
executableShimPaths: executableShimPathsForJetBrainsIDE('idea'),
|
||||
expectedInstallationChecker: (displayName, publisher) =>
|
||||
displayName.startsWith('IntelliJ IDEA Community Edition ') &&
|
||||
publisher === 'JetBrains s.r.o.',
|
||||
},
|
||||
]
|
||||
|
||||
function getKeyOrEmpty(
|
||||
|
@ -332,14 +374,15 @@ async function findApplication(editor: IWindowsExternalEditor) {
|
|||
continue
|
||||
}
|
||||
|
||||
const path = Path.join(installLocation, ...editor.executableShimPath)
|
||||
const exists = await pathExists(path)
|
||||
if (!exists) {
|
||||
log.debug(`Executable for ${editor.name} not found at '${path}'`)
|
||||
continue
|
||||
}
|
||||
for (const executableShimPath of editor.executableShimPaths) {
|
||||
const path = Path.join(installLocation, ...executableShimPath)
|
||||
const exists = await pathExists(path)
|
||||
if (exists) {
|
||||
return path
|
||||
}
|
||||
|
||||
return path
|
||||
log.debug(`Executable for ${editor.name} not found at '${path}'`)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
|
|
|
@ -47,11 +47,6 @@ export function enableHideWhitespaceInDiffOption(): boolean {
|
|||
return true
|
||||
}
|
||||
|
||||
/** Should the app use the shiny new TCP-based trampoline? */
|
||||
export function enableDesktopTrampoline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Should we use the new diff viewer for unified diffs?
|
||||
*/
|
||||
|
@ -135,5 +130,12 @@ export function enableWindowsOpenSSH(): boolean {
|
|||
|
||||
/** Should we use SSH askpass? */
|
||||
export function enableSSHAskPass(): boolean {
|
||||
return __WIN32__ && enableBetaFeatures()
|
||||
return enableBetaFeatures()
|
||||
}
|
||||
|
||||
/** Should we use the setImmediate alternative? */
|
||||
export function enableSetAlmostImmediate(): boolean {
|
||||
// We only noticed the problem with `setImmediate` on macOS, so no need to
|
||||
// use this trick on Windows for now.
|
||||
return __DARWIN__ && enableBetaFeatures()
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ export async function deleteRemoteBranch(
|
|||
// If the user is not authenticated, the push is going to fail
|
||||
// Let this propagate and leave it to the caller to handle
|
||||
const result = await git(args, repository.path, 'deleteRemoteBranch', {
|
||||
env: envForRemoteOperation(account, remoteUrl),
|
||||
env: await envForRemoteOperation(account, remoteUrl),
|
||||
expectedErrors: new Set<DugiteError>([DugiteError.BranchDeletionFailed]),
|
||||
})
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import { isErrnoException } from '../errno-exception'
|
|||
import { ChildProcess } from 'child_process'
|
||||
import { Readable } from 'stream'
|
||||
import split2 from 'split2'
|
||||
import { getSSHEnvironment } from '../ssh/ssh'
|
||||
import { merge } from '../merge'
|
||||
import { withTrampolineEnv } from '../trampoline/trampoline-environment'
|
||||
|
||||
|
@ -160,9 +159,8 @@ export async function git(
|
|||
combineOutput(process.stdout)
|
||||
}
|
||||
|
||||
const result = await withTrampolineEnv(async env => {
|
||||
const sshEnvironment = await getSSHEnvironment()
|
||||
const combinedEnv = merge(opts.env, merge(env, sshEnvironment))
|
||||
return withTrampolineEnv(async env => {
|
||||
const combinedEnv = merge(opts.env, env)
|
||||
|
||||
// Explicitly set TERM to 'dumb' so that if Desktop was launched
|
||||
// from a terminal or if the system environment variables
|
||||
|
@ -172,7 +170,7 @@ export async function git(
|
|||
|
||||
const commandName = `${name}: git ${args.join(' ')}`
|
||||
|
||||
return GitPerf.measure(commandName, () =>
|
||||
const result = await GitPerf.measure(commandName, () =>
|
||||
GitProcess.exec(args, path, opts)
|
||||
).catch(err => {
|
||||
// If this is an exception thrown by Node.js (as opposed to
|
||||
|
@ -184,64 +182,66 @@ export async function git(
|
|||
|
||||
throw err
|
||||
})
|
||||
})
|
||||
|
||||
const exitCode = result.exitCode
|
||||
const exitCode = result.exitCode
|
||||
|
||||
let gitError: DugiteError | null = null
|
||||
const acceptableExitCode = opts.successExitCodes
|
||||
? opts.successExitCodes.has(exitCode)
|
||||
: false
|
||||
if (!acceptableExitCode) {
|
||||
gitError = GitProcess.parseError(result.stderr)
|
||||
if (!gitError) {
|
||||
gitError = GitProcess.parseError(result.stdout)
|
||||
let gitError: DugiteError | null = null
|
||||
const acceptableExitCode = opts.successExitCodes
|
||||
? opts.successExitCodes.has(exitCode)
|
||||
: false
|
||||
if (!acceptableExitCode) {
|
||||
gitError = GitProcess.parseError(result.stderr)
|
||||
if (!gitError) {
|
||||
gitError = GitProcess.parseError(result.stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const gitErrorDescription = gitError ? getDescriptionForError(gitError) : null
|
||||
const gitResult = {
|
||||
...result,
|
||||
gitError,
|
||||
gitErrorDescription,
|
||||
combinedOutput,
|
||||
path,
|
||||
}
|
||||
const gitErrorDescription = gitError
|
||||
? getDescriptionForError(gitError)
|
||||
: null
|
||||
const gitResult = {
|
||||
...result,
|
||||
gitError,
|
||||
gitErrorDescription,
|
||||
combinedOutput,
|
||||
path,
|
||||
}
|
||||
|
||||
let acceptableError = true
|
||||
if (gitError && opts.expectedErrors) {
|
||||
acceptableError = opts.expectedErrors.has(gitError)
|
||||
}
|
||||
let acceptableError = true
|
||||
if (gitError && opts.expectedErrors) {
|
||||
acceptableError = opts.expectedErrors.has(gitError)
|
||||
}
|
||||
|
||||
if ((gitError && acceptableError) || acceptableExitCode) {
|
||||
return gitResult
|
||||
}
|
||||
if ((gitError && acceptableError) || acceptableExitCode) {
|
||||
return gitResult
|
||||
}
|
||||
|
||||
// The caller should either handle this error, or expect that exit code.
|
||||
const errorMessage = new Array<string>()
|
||||
errorMessage.push(
|
||||
`\`git ${args.join(' ')}\` exited with an unexpected code: ${exitCode}.`
|
||||
)
|
||||
|
||||
if (result.stdout) {
|
||||
errorMessage.push('stdout:')
|
||||
errorMessage.push(result.stdout)
|
||||
}
|
||||
|
||||
if (result.stderr) {
|
||||
errorMessage.push('stderr:')
|
||||
errorMessage.push(result.stderr)
|
||||
}
|
||||
|
||||
if (gitError) {
|
||||
// The caller should either handle this error, or expect that exit code.
|
||||
const errorMessage = new Array<string>()
|
||||
errorMessage.push(
|
||||
`(The error was parsed as ${gitError}: ${gitErrorDescription})`
|
||||
`\`git ${args.join(' ')}\` exited with an unexpected code: ${exitCode}.`
|
||||
)
|
||||
}
|
||||
|
||||
log.error(errorMessage.join('\n'))
|
||||
if (result.stdout) {
|
||||
errorMessage.push('stdout:')
|
||||
errorMessage.push(result.stdout)
|
||||
}
|
||||
|
||||
throw new GitError(gitResult, args)
|
||||
if (result.stderr) {
|
||||
errorMessage.push('stderr:')
|
||||
errorMessage.push(result.stderr)
|
||||
}
|
||||
|
||||
if (gitError) {
|
||||
errorMessage.push(
|
||||
`(The error was parsed as ${gitError}: ${gitErrorDescription})`
|
||||
)
|
||||
}
|
||||
|
||||
log.error(errorMessage.join('\n'))
|
||||
|
||||
throw new GitError(gitResult, args)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -63,7 +63,7 @@ export async function fetch(
|
|||
): Promise<void> {
|
||||
let opts: IGitExecutionOptions = {
|
||||
successExitCodes: new Set([0]),
|
||||
env: envForRemoteOperation(account, remote.url),
|
||||
env: await envForRemoteOperation(account, remote.url),
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
|
@ -121,7 +121,7 @@ export async function fetchRefspec(
|
|||
): Promise<void> {
|
||||
const options = {
|
||||
successExitCodes: new Set([0, 128]),
|
||||
env: envForRemoteOperation(account, remote.url),
|
||||
env: await envForRemoteOperation(account, remote.url),
|
||||
}
|
||||
|
||||
const networkArguments = await gitNetworkArguments(repository, account)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { GitProcess } from 'dugite'
|
||||
import * as GitPerf from '../../ui/lib/git-perf'
|
||||
import { isErrnoException } from '../errno-exception'
|
||||
import { getSSHEnvironment } from '../ssh/ssh'
|
||||
import { withTrampolineEnv } from '../trampoline/trampoline-environment'
|
||||
|
||||
type ProcessOutput = {
|
||||
|
@ -39,13 +38,7 @@ export async function spawnAndComplete(
|
|||
commandName,
|
||||
() =>
|
||||
new Promise<ProcessOutput>(async (resolve, reject) => {
|
||||
const sshEnv = await getSSHEnvironment()
|
||||
const process = GitProcess.spawn(args, path, {
|
||||
env: {
|
||||
...env,
|
||||
...sshEnv,
|
||||
},
|
||||
})
|
||||
const process = GitProcess.spawn(args, path, { env })
|
||||
|
||||
process.on('error', err => {
|
||||
// If this is an exception thrown by Node.js while attempting to
|
||||
|
|
7
app/src/lib/globals.d.ts
vendored
7
app/src/lib/globals.d.ts
vendored
|
@ -39,12 +39,7 @@ declare const __UPDATES_URL__: string
|
|||
* The currently executing process kind, this is specific to desktop
|
||||
* and identifies the processes that we have.
|
||||
*/
|
||||
declare const __PROCESS_KIND__:
|
||||
| 'main'
|
||||
| 'ui'
|
||||
| 'crash'
|
||||
| 'askpass'
|
||||
| 'highlighter'
|
||||
declare const __PROCESS_KIND__: 'main' | 'ui' | 'crash' | 'highlighter'
|
||||
|
||||
/**
|
||||
* The IdleDeadline interface is used as the data type of the input parameter to
|
||||
|
|
35
app/src/lib/set-almost-immediate.ts
Normal file
35
app/src/lib/set-almost-immediate.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/* eslint-disable set-almost-immediate */
|
||||
import { enableSetAlmostImmediate } from './feature-flag'
|
||||
|
||||
/**
|
||||
* Reference created by setAlmostImmediate so that it can be cleared later.
|
||||
* It can be NodeJS.Immediate or number because we use a feature flag
|
||||
* to tweak the behavior of setAlmostImmediate, but this type should be used
|
||||
* as if it were opaque.
|
||||
*/
|
||||
export type AlmostImmediate = NodeJS.Immediate | number
|
||||
|
||||
/**
|
||||
* This function behaves almost like setImmediate, but it will rely on
|
||||
* setTimeout(..., 0) to actually execute the callback. The reason for this
|
||||
* is a bug in Electron sometimes causing setImmediate callbacks to not being
|
||||
* executed.
|
||||
* For more info about this: https://github.com/electron/electron/issues/29261
|
||||
*/
|
||||
export function setAlmostImmediate(
|
||||
callback: (...args: any[]) => void,
|
||||
...args: any[]
|
||||
): AlmostImmediate {
|
||||
return enableSetAlmostImmediate()
|
||||
? window.setTimeout(callback, 0, ...args)
|
||||
: setImmediate(callback, ...args)
|
||||
}
|
||||
|
||||
/** Used to clear references created by setAlmostImmediate. */
|
||||
export function clearAlmostImmediate(almostImmediate: AlmostImmediate) {
|
||||
if (typeof almostImmediate === 'number') {
|
||||
clearTimeout(almostImmediate)
|
||||
} else {
|
||||
clearImmediate(almostImmediate)
|
||||
}
|
||||
}
|
78
app/src/lib/ssh/ssh-key-passphrase.ts
Normal file
78
app/src/lib/ssh/ssh-key-passphrase.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { getFileHash } from '../file-system'
|
||||
import { TokenStore } from '../stores'
|
||||
|
||||
const appName = __DEV__ ? 'GitHub Desktop Dev' : 'GitHub Desktop'
|
||||
const SSHKeyPassphraseTokenStoreKey = `${appName} - SSH key passphrases`
|
||||
|
||||
async function getHashForSSHKey(keyPath: string) {
|
||||
return getFileHash(keyPath, 'sha256')
|
||||
}
|
||||
|
||||
/** Retrieves the passphrase for the SSH key in the given path. */
|
||||
export async function getSSHKeyPassphrase(keyPath: string) {
|
||||
try {
|
||||
const fileHash = await getHashForSSHKey(keyPath)
|
||||
return TokenStore.getItem(SSHKeyPassphraseTokenStoreKey, fileHash)
|
||||
} catch (e) {
|
||||
log.error('Could not retrieve passphrase for SSH key:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
type SSHKeyPassphraseEntry = {
|
||||
/** Hash of the SSH key file. */
|
||||
keyHash: string
|
||||
|
||||
/** Passphrase for the SSH key. */
|
||||
passphrase: string
|
||||
}
|
||||
|
||||
/**
|
||||
* This map contains the SSH key passphrases that are pending to be stored.
|
||||
* What this means is that a git operation is currently in progress, and the
|
||||
* user wanted to store the passphrase for the SSH key, however we don't want
|
||||
* to store it until we know the git operation finished successfully.
|
||||
*/
|
||||
const SSHKeyPassphrasesToStore = new Map<string, SSHKeyPassphraseEntry>()
|
||||
|
||||
/**
|
||||
* Keeps the SSH key passphrase in memory to be stored later if the ongoing git
|
||||
* operation succeeds.
|
||||
*
|
||||
* @param operationGUID A unique identifier for the ongoing git operation. In
|
||||
* practice, it will always be the trampoline token for the
|
||||
* ongoing git operation.
|
||||
* @param keyPath Path to the SSH key.
|
||||
* @param passphrase Passphrase for the SSH key.
|
||||
*/
|
||||
export async function keepSSHKeyPassphraseToStore(
|
||||
operationGUID: string,
|
||||
keyPath: string,
|
||||
passphrase: string
|
||||
) {
|
||||
try {
|
||||
const keyHash = await getHashForSSHKey(keyPath)
|
||||
SSHKeyPassphrasesToStore.set(operationGUID, { keyHash, passphrase })
|
||||
} catch (e) {
|
||||
log.error('Could not store passphrase for SSH key:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes the SSH key passphrase from memory. */
|
||||
export function removePendingSSHKeyPassphraseToStore(operationGUID: string) {
|
||||
SSHKeyPassphrasesToStore.delete(operationGUID)
|
||||
}
|
||||
|
||||
/** Stores a pending SSH key passphrase if the operation succeeded. */
|
||||
export async function storePendingSSHKeyPassphrase(operationGUID: string) {
|
||||
const entry = SSHKeyPassphrasesToStore.get(operationGUID)
|
||||
if (entry === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await TokenStore.setItem(
|
||||
SSHKeyPassphraseTokenStoreKey,
|
||||
entry.keyHash,
|
||||
entry.passphrase
|
||||
)
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import * as fse from 'fs-extra'
|
||||
import memoizeOne from 'memoize-one'
|
||||
import { enableSSHAskPass, enableWindowsOpenSSH } from '../feature-flag'
|
||||
import { getFileHash } from '../file-system'
|
||||
import { getBoolean } from '../local-storage'
|
||||
import { TokenStore } from '../stores'
|
||||
import { getDesktopTrampolinePath } from '../trampoline/trampoline-environment'
|
||||
import {
|
||||
getDesktopTrampolinePath,
|
||||
getSSHWrapperPath,
|
||||
} from '../trampoline/trampoline-environment'
|
||||
|
||||
const WindowsOpenSSHPath = 'C:/Windows/System32/OpenSSH/ssh.exe'
|
||||
|
||||
|
@ -45,6 +46,8 @@ export async function getSSHEnvironment() {
|
|||
const baseEnv = enableSSHAskPass()
|
||||
? {
|
||||
SSH_ASKPASS: getDesktopTrampolinePath(),
|
||||
// DISPLAY needs to be set to _something_ so ssh actually uses SSH_ASKPASS
|
||||
DISPLAY: '.',
|
||||
}
|
||||
: {}
|
||||
|
||||
|
@ -57,40 +60,14 @@ export async function getSSHEnvironment() {
|
|||
}
|
||||
}
|
||||
|
||||
if (__DARWIN__ && __DEV__ && enableSSHAskPass()) {
|
||||
// Replace git ssh command with our wrapper in dev builds, since they are
|
||||
// launched from a command line.
|
||||
return {
|
||||
...baseEnv,
|
||||
GIT_SSH_COMMAND: `"${getSSHWrapperPath()}"`,
|
||||
}
|
||||
}
|
||||
|
||||
return baseEnv
|
||||
}
|
||||
|
||||
const appName = __DEV__ ? 'GitHub Desktop Dev' : 'GitHub'
|
||||
const SSHKeyPassphraseTokenStoreKey = `${appName} - SSH key passphrases`
|
||||
|
||||
async function getHashForSSHKey(keyPath: string) {
|
||||
return getFileHash(keyPath, 'sha256')
|
||||
}
|
||||
|
||||
/** Retrieves the passphrase for the SSH key in the given path. */
|
||||
export async function getSSHKeyPassphrase(keyPath: string) {
|
||||
try {
|
||||
const fileHash = await getHashForSSHKey(keyPath)
|
||||
return TokenStore.getItem(SSHKeyPassphraseTokenStoreKey, fileHash)
|
||||
} catch (e) {
|
||||
log.error('Could not retrieve passphrase for SSH key:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Stores the passphrase for the SSH key in the given path. */
|
||||
export async function storeSSHKeyPassphrase(
|
||||
keyPath: string,
|
||||
passphrase: string
|
||||
) {
|
||||
try {
|
||||
const fileHash = await getHashForSSHKey(keyPath)
|
||||
await TokenStore.setItem(
|
||||
SSHKeyPassphraseTokenStoreKey,
|
||||
fileHash,
|
||||
passphrase
|
||||
)
|
||||
} catch (e) {
|
||||
log.error('Could not store passphrase for SSH key:', e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
APICheckConclusion,
|
||||
} from '../api'
|
||||
import { IDisposable, Disposable } from 'event-kit'
|
||||
import { setAlmostImmediate } from '../set-almost-immediate'
|
||||
|
||||
/**
|
||||
* A Desktop-specific model closely related to a GitHub API Check Run.
|
||||
|
@ -225,7 +226,7 @@ export class CommitStatusStore {
|
|||
private queueRefresh() {
|
||||
if (!this.refreshQueued) {
|
||||
this.refreshQueued = true
|
||||
setImmediate(() => {
|
||||
setAlmostImmediate(() => {
|
||||
this.refreshQueued = false
|
||||
this.refreshEligibleSubscriptions()
|
||||
})
|
||||
|
|
|
@ -23,6 +23,7 @@ import { clearTagsToPush } from './helpers/tags-to-push-storage'
|
|||
import { IMatchedGitHubRepository } from '../repository-matching'
|
||||
import { shallowEquals } from '../equality'
|
||||
import { enableRepositoryAliases } from '../feature-flag'
|
||||
import { setAlmostImmediate } from '../set-almost-immediate'
|
||||
|
||||
/** The store for local repositories. */
|
||||
export class RepositoriesStore extends TypedBaseStore<
|
||||
|
@ -663,7 +664,7 @@ export class RepositoriesStore extends TypedBaseStore<
|
|||
*/
|
||||
private emitUpdatedRepositories() {
|
||||
if (!this.emitQueued) {
|
||||
setImmediate(() => {
|
||||
setAlmostImmediate(() => {
|
||||
this.getAll()
|
||||
.then(repos => this.emitUpdate(repos))
|
||||
.catch(e => log.error(`Failed emitting update`, e))
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { getKeyForEndpoint } from '../auth'
|
||||
import { getSSHKeyPassphrase } from '../ssh/ssh'
|
||||
import {
|
||||
getSSHKeyPassphrase,
|
||||
keepSSHKeyPassphraseToStore,
|
||||
removePendingSSHKeyPassphraseToStore,
|
||||
} from '../ssh/ssh-key-passphrase'
|
||||
import { TokenStore } from '../stores'
|
||||
import { TrampolineCommandHandler } from './trampoline-command'
|
||||
import { trampolineUIHelper } from './trampoline-ui-helper'
|
||||
|
@ -37,6 +41,7 @@ async function handleSSHHostAuthenticity(
|
|||
}
|
||||
|
||||
async function handleSSHKeyPassphrase(
|
||||
operationGUID: string,
|
||||
prompt: string
|
||||
): Promise<string | undefined> {
|
||||
const promptRegex = /^Enter passphrase for key '(.+)': $/
|
||||
|
@ -62,7 +67,23 @@ async function handleSSHKeyPassphrase(
|
|||
return storedPassphrase
|
||||
}
|
||||
|
||||
const passphrase = await trampolineUIHelper.promptSSHKeyPassphrase(keyPath)
|
||||
const {
|
||||
passphrase,
|
||||
storePassphrase,
|
||||
} = await trampolineUIHelper.promptSSHKeyPassphrase(keyPath)
|
||||
|
||||
// If the user wanted us to remember the passphrase, we'll keep it around to
|
||||
// store it later if the git operation succeeds.
|
||||
// However, when running a git command, it's possible that the user will need
|
||||
// to enter the passphrase multiple times if there are failed attempts.
|
||||
// Because of that, we need to remove any pending passphrases to be stored
|
||||
// when, in one of those multiple attempts, the user chooses NOT to remember
|
||||
// the passphrase.
|
||||
if (passphrase !== undefined && storePassphrase) {
|
||||
keepSSHKeyPassphraseToStore(operationGUID, keyPath, passphrase)
|
||||
} else {
|
||||
removePendingSSHKeyPassphraseToStore(operationGUID)
|
||||
}
|
||||
|
||||
return passphrase ?? ''
|
||||
}
|
||||
|
@ -79,7 +100,7 @@ export const askpassTrampolineHandler: TrampolineCommandHandler = async command
|
|||
}
|
||||
|
||||
if (firstParameter.startsWith('Enter passphrase for key ')) {
|
||||
return handleSSHKeyPassphrase(firstParameter)
|
||||
return handleSSHKeyPassphrase(command.trampolineToken, firstParameter)
|
||||
}
|
||||
|
||||
const username = command.environmentVariables.get('DESKTOP_USERNAME')
|
||||
|
|
|
@ -126,8 +126,17 @@ export class TrampolineCommandParser {
|
|||
)
|
||||
}
|
||||
|
||||
const trampolineToken = this.environmentVariables.get(
|
||||
'DESKTOP_TRAMPOLINE_TOKEN'
|
||||
)
|
||||
|
||||
if (trampolineToken === undefined) {
|
||||
throw new Error(`The trampoline token is missing`)
|
||||
}
|
||||
|
||||
return {
|
||||
identifier,
|
||||
trampolineToken,
|
||||
parameters: this.parameters,
|
||||
environmentVariables: this.environmentVariables,
|
||||
}
|
||||
|
|
|
@ -12,6 +12,12 @@ export interface ITrampolineCommand {
|
|||
*/
|
||||
readonly identifier: TrampolineCommandIdentifier
|
||||
|
||||
/**
|
||||
* Trampoline token sent with this command via the DESKTOP_TRAMPOLINE_TOKEN
|
||||
* environment variable.
|
||||
*/
|
||||
readonly trampolineToken: string
|
||||
|
||||
/**
|
||||
* Parameters of the command.
|
||||
*
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import { trampolineServer } from './trampoline-server'
|
||||
import { withTrampolineToken } from './trampoline-tokens'
|
||||
import * as Path from 'path'
|
||||
import { enableDesktopTrampoline } from '../feature-flag'
|
||||
import { getDesktopTrampolineFilename } from 'desktop-trampoline'
|
||||
import { TrampolineCommandIdentifier } from '../trampoline/trampoline-command'
|
||||
import { getSSHEnvironment } from '../ssh/ssh'
|
||||
import {
|
||||
removePendingSSHKeyPassphraseToStore,
|
||||
storePendingSSHKeyPassphrase,
|
||||
} from '../ssh/ssh-key-passphrase'
|
||||
|
||||
/**
|
||||
* Allows invoking a function with a set of environment variables to use when
|
||||
* invoking a Git subcommand that needs to use the trampoline (mainly git
|
||||
* operations requiring an askpass script) and with a token to use in the
|
||||
* trampoline server.
|
||||
* It will handle saving SSH key passphrases when needed if the git operation
|
||||
* succeeds.
|
||||
*
|
||||
* @param fn Function to invoke with all the necessary environment
|
||||
* variables.
|
||||
|
@ -17,22 +23,36 @@ import { TrampolineCommandIdentifier } from '../trampoline/trampoline-command'
|
|||
export async function withTrampolineEnv<T>(
|
||||
fn: (env: Object) => Promise<T>
|
||||
): Promise<T> {
|
||||
const askPassPath = enableDesktopTrampoline()
|
||||
? getDesktopTrampolinePath()
|
||||
: getAskPassTrampolinePath()
|
||||
const sshEnv = await getSSHEnvironment()
|
||||
|
||||
return withTrampolineToken(async token =>
|
||||
fn({
|
||||
DESKTOP_PORT: await trampolineServer.getPort(),
|
||||
DESKTOP_TRAMPOLINE_TOKEN: token,
|
||||
GIT_ASKPASS: askPassPath,
|
||||
DESKTOP_TRAMPOLINE_IDENTIFIER: TrampolineCommandIdentifier.AskPass,
|
||||
return withTrampolineToken(async token => {
|
||||
// The code below assumes a few things in order to manage SSH key passphrases
|
||||
// correctly:
|
||||
// 1. `withTrampolineEnv` is only used in the functions `git` (core.ts) and
|
||||
// `spawnAndComplete` (spawn.ts)
|
||||
// 2. Those two functions always thrown an error when something went wrong,
|
||||
// and just return a result when everything went fine.
|
||||
//
|
||||
// With those two premises in mind, we can safely assume that right after
|
||||
// `fn` has been invoked, we can store the SSH key passphrase for this git
|
||||
// operation if there was one pending to be stored.
|
||||
try {
|
||||
const result = await fn({
|
||||
DESKTOP_PORT: await trampolineServer.getPort(),
|
||||
DESKTOP_TRAMPOLINE_TOKEN: token,
|
||||
GIT_ASKPASS: getDesktopTrampolinePath(),
|
||||
DESKTOP_TRAMPOLINE_IDENTIFIER: TrampolineCommandIdentifier.AskPass,
|
||||
|
||||
// Env variables specific to the old askpass trampoline
|
||||
DESKTOP_PATH: process.execPath,
|
||||
DESKTOP_ASKPASS_SCRIPT: getAskPassScriptPath(),
|
||||
})
|
||||
)
|
||||
...sshEnv,
|
||||
})
|
||||
|
||||
await storePendingSSHKeyPassphrase(token)
|
||||
|
||||
return result
|
||||
} finally {
|
||||
removePendingSSHKeyPassphraseToStore(token)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Returns the path of the desktop-trampoline binary. */
|
||||
|
@ -44,11 +64,7 @@ export function getDesktopTrampolinePath(): string {
|
|||
)
|
||||
}
|
||||
|
||||
function getAskPassTrampolinePath(): string {
|
||||
const extension = __WIN32__ ? 'bat' : 'sh'
|
||||
return Path.resolve(__dirname, 'static', `ask-pass-trampoline.${extension}`)
|
||||
}
|
||||
|
||||
function getAskPassScriptPath(): string {
|
||||
return Path.resolve(__dirname, 'ask-pass.js')
|
||||
/** Returns the path of the ssh-wrapper binary. */
|
||||
export function getSSHWrapperPath(): string {
|
||||
return Path.resolve(__dirname, 'desktop-trampoline', 'ssh-wrapper')
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { createServer, AddressInfo, Server, Socket } from 'net'
|
||||
import split2 from 'split2'
|
||||
import { enableDesktopTrampoline } from '../feature-flag'
|
||||
import { sendNonFatalException } from '../helpers/non-fatal-exception'
|
||||
import { askpassTrampolineHandler } from './trampoline-askpass-handler'
|
||||
import {
|
||||
|
@ -51,11 +50,6 @@ export class TrampolineServer {
|
|||
}
|
||||
|
||||
private async listen(): Promise<void> {
|
||||
if (!enableDesktopTrampoline()) {
|
||||
this.listeningPromise = Promise.resolve()
|
||||
return this.listeningPromise
|
||||
}
|
||||
|
||||
this.listeningPromise = new Promise((resolve, reject) => {
|
||||
// Observe errors while trying to start the server
|
||||
this.server.on('error', error => {
|
||||
|
@ -157,9 +151,7 @@ export class TrampolineServer {
|
|||
}
|
||||
|
||||
private async processCommand(socket: Socket, command: ITrampolineCommand) {
|
||||
const token = command.environmentVariables.get('DESKTOP_TRAMPOLINE_TOKEN')
|
||||
|
||||
if (token === undefined || !isValidTrampolineToken(token)) {
|
||||
if (!isValidTrampolineToken(command.trampolineToken)) {
|
||||
throw new Error('Tried to use invalid trampoline token')
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { PopupType } from '../../models/popup'
|
||||
import { Dispatcher } from '../../ui/dispatcher'
|
||||
|
||||
type PromptSSHKeyPassphraseResponse = {
|
||||
readonly passphrase: string | undefined
|
||||
readonly storePassphrase: boolean
|
||||
}
|
||||
|
||||
class TrampolineUIHelper {
|
||||
// The dispatcher must be set before this helper can do anything
|
||||
private dispatcher!: Dispatcher
|
||||
|
@ -25,12 +30,15 @@ class TrampolineUIHelper {
|
|||
})
|
||||
}
|
||||
|
||||
public promptSSHKeyPassphrase(keyPath: string): Promise<string | undefined> {
|
||||
public promptSSHKeyPassphrase(
|
||||
keyPath: string
|
||||
): Promise<PromptSSHKeyPassphraseResponse> {
|
||||
return new Promise(resolve => {
|
||||
this.dispatcher.showPopup({
|
||||
type: PopupType.SSHKeyPassphrase,
|
||||
keyPath,
|
||||
onSubmit: passphrase => resolve(passphrase),
|
||||
onSubmit: (passphrase, storePassphrase) =>
|
||||
resolve({ passphrase, storePassphrase }),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -303,5 +303,8 @@ export type Popup =
|
|||
| {
|
||||
type: PopupType.SSHKeyPassphrase
|
||||
keyPath: string
|
||||
onSubmit: (passphrase: string | undefined) => void
|
||||
onSubmit: (
|
||||
passphrase: string | undefined,
|
||||
storePassphrase: boolean
|
||||
) => void
|
||||
}
|
||||
|
|
|
@ -141,6 +141,7 @@ import { AddSSHHost } from './ssh/add-ssh-host'
|
|||
import { SSHKeyPassphrase } from './ssh/ssh-key-passphrase'
|
||||
import { getMultiCommitOperationChooseBranchStep } from '../lib/multi-commit-operation'
|
||||
import { ConfirmForcePush } from './rebase/confirm-force-push'
|
||||
import { setAlmostImmediate } from '../lib/set-almost-immediate'
|
||||
|
||||
const MinuteInMilliseconds = 1000 * 60
|
||||
const HourInMilliseconds = MinuteInMilliseconds * 60
|
||||
|
@ -541,7 +542,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
}
|
||||
|
||||
private boomtown() {
|
||||
setImmediate(() => {
|
||||
setAlmostImmediate(() => {
|
||||
throw new Error('Boomtown!')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,6 +8,11 @@ import { OnionSkin } from './onion-skin'
|
|||
import { Swipe } from './swipe'
|
||||
import { assertNever } from '../../../lib/fatal-error'
|
||||
import { ISize, getMaxFitSize } from './sizing'
|
||||
import {
|
||||
AlmostImmediate,
|
||||
clearAlmostImmediate,
|
||||
setAlmostImmediate,
|
||||
} from '../../../lib/set-almost-immediate'
|
||||
|
||||
interface IModifiedImageDiffProps {
|
||||
readonly previous: Image
|
||||
|
@ -63,7 +68,7 @@ export class ModifiedImageDiff extends React.Component<
|
|||
private container: HTMLElement | null = null
|
||||
|
||||
private readonly resizeObserver: ResizeObserver
|
||||
private resizedTimeoutID: NodeJS.Immediate | null = null
|
||||
private resizedTimeoutID: AlmostImmediate | null = null
|
||||
|
||||
public constructor(props: IModifiedImageDiffProps) {
|
||||
super(props)
|
||||
|
@ -75,10 +80,10 @@ export class ModifiedImageDiff extends React.Component<
|
|||
// when we're reacting to a resize so we'll defer it until after
|
||||
// react is done with this frame.
|
||||
if (this.resizedTimeoutID !== null) {
|
||||
clearImmediate(this.resizedTimeoutID)
|
||||
clearAlmostImmediate(this.resizedTimeoutID)
|
||||
}
|
||||
|
||||
this.resizedTimeoutID = setImmediate(
|
||||
this.resizedTimeoutID = setAlmostImmediate(
|
||||
this.onResized,
|
||||
entry.target,
|
||||
entry.contentRect
|
||||
|
|
|
@ -14,6 +14,11 @@ import { wrapRichTextCommitMessage } from '../../lib/wrap-rich-text-commit-messa
|
|||
import { DiffOptions } from '../diff/diff-options'
|
||||
import { RepositorySectionTab } from '../../lib/app-state'
|
||||
import { IChangesetData } from '../../lib/git'
|
||||
import {
|
||||
AlmostImmediate,
|
||||
clearAlmostImmediate,
|
||||
setAlmostImmediate,
|
||||
} from '../../lib/set-almost-immediate'
|
||||
|
||||
interface ICommitSummaryProps {
|
||||
readonly repository: Repository
|
||||
|
@ -124,7 +129,7 @@ export class CommitSummary extends React.Component<
|
|||
> {
|
||||
private descriptionScrollViewRef: HTMLDivElement | null = null
|
||||
private readonly resizeObserver: ResizeObserver | null = null
|
||||
private updateOverflowTimeoutId: NodeJS.Immediate | null = null
|
||||
private updateOverflowTimeoutId: AlmostImmediate | null = null
|
||||
private descriptionRef: HTMLDivElement | null = null
|
||||
|
||||
public constructor(props: ICommitSummaryProps) {
|
||||
|
@ -143,10 +148,10 @@ export class CommitSummary extends React.Component<
|
|||
// when we're reacting to a resize so we'll defer it until after
|
||||
// react is done with this frame.
|
||||
if (this.updateOverflowTimeoutId !== null) {
|
||||
clearImmediate(this.updateOverflowTimeoutId)
|
||||
clearAlmostImmediate(this.updateOverflowTimeoutId)
|
||||
}
|
||||
|
||||
this.updateOverflowTimeoutId = setImmediate(this.onResized)
|
||||
this.updateOverflowTimeoutId = setAlmostImmediate(this.onResized)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -17,6 +17,11 @@ import { createUniqueId, releaseUniqueId } from '../../lib/id-pool'
|
|||
import { range } from '../../../lib/range'
|
||||
import { ListItemInsertionOverlay } from './list-item-insertion-overlay'
|
||||
import { DragData, DragType } from '../../../models/drag-drop'
|
||||
import {
|
||||
AlmostImmediate,
|
||||
clearAlmostImmediate,
|
||||
setAlmostImmediate,
|
||||
} from '../../../lib/set-almost-immediate'
|
||||
|
||||
/**
|
||||
* Describe the first argument given to the cellRenderer,
|
||||
|
@ -276,7 +281,7 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
private list: HTMLDivElement | null = null
|
||||
private grid: Grid | null = null
|
||||
private readonly resizeObserver: ResizeObserver | null = null
|
||||
private updateSizeTimeoutId: NodeJS.Immediate | null = null
|
||||
private updateSizeTimeoutId: AlmostImmediate | null = null
|
||||
|
||||
public constructor(props: IListProps) {
|
||||
super(props)
|
||||
|
@ -294,10 +299,10 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
// when we're reacting to a resize so we'll defer it until after
|
||||
// react is done with this frame.
|
||||
if (this.updateSizeTimeoutId !== null) {
|
||||
clearImmediate(this.updateSizeTimeoutId)
|
||||
clearAlmostImmediate(this.updateSizeTimeoutId)
|
||||
}
|
||||
|
||||
this.updateSizeTimeoutId = setImmediate(
|
||||
this.updateSizeTimeoutId = setAlmostImmediate(
|
||||
this.onResized,
|
||||
entry.target,
|
||||
entry.contentRect
|
||||
|
@ -774,7 +779,7 @@ export class List extends React.Component<IListProps, IListState> {
|
|||
|
||||
public componentWillUnmount() {
|
||||
if (this.updateSizeTimeoutId !== null) {
|
||||
clearImmediate(this.updateSizeTimeoutId)
|
||||
clearAlmostImmediate(this.updateSizeTimeoutId)
|
||||
this.updateSizeTimeoutId = null
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ export class WarnForcePushDialog extends React.Component<
|
|||
|
||||
const title = __DARWIN__
|
||||
? `${operation} Will Require Force Push`
|
||||
: `${operation} will require force push'`
|
||||
: `${operation} will require force push`
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
|
|
@ -4,11 +4,13 @@ import { Row } from '../lib/row'
|
|||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { TextBox } from '../lib/text-box'
|
||||
import { Checkbox, CheckboxValue } from '../lib/checkbox'
|
||||
import { storeSSHKeyPassphrase } from '../../lib/ssh/ssh'
|
||||
|
||||
interface ISSHKeyPassphraseProps {
|
||||
readonly keyPath: string
|
||||
readonly onSubmit: (passphrase: string | undefined) => void
|
||||
readonly onSubmit: (
|
||||
passphrase: string | undefined,
|
||||
storePassphrase: boolean
|
||||
) => void
|
||||
readonly onDismissed: () => void
|
||||
}
|
||||
|
||||
|
@ -80,21 +82,18 @@ export class SSHKeyPassphrase extends React.Component<
|
|||
this.setState({ passphrase: value })
|
||||
}
|
||||
|
||||
private submit(passphrase: string | undefined) {
|
||||
private submit(passphrase: string | undefined, storePassphrase: boolean) {
|
||||
const { onSubmit, onDismissed } = this.props
|
||||
|
||||
onSubmit(passphrase)
|
||||
onSubmit(passphrase, storePassphrase)
|
||||
onDismissed()
|
||||
}
|
||||
|
||||
private onSubmit = () => {
|
||||
if (this.state.rememberPassphrase) {
|
||||
storeSSHKeyPassphrase(this.props.keyPath, this.state.passphrase)
|
||||
}
|
||||
this.submit(this.state.passphrase)
|
||||
this.submit(this.state.passphrase, this.state.rememberPassphrase)
|
||||
}
|
||||
|
||||
private onCancel = () => {
|
||||
this.submit(undefined)
|
||||
this.submit(undefined, false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
ELECTRON_RUN_AS_NODE=1 "$DESKTOP_PATH" "$DESKTOP_ASKPASS_SCRIPT" "$@"
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
ELECTRON_RUN_AS_NODE=1 "$DESKTOP_PATH" "$DESKTOP_ASKPASS_SCRIPT" "$@"
|
|
@ -1,9 +0,0 @@
|
|||
@echo off
|
||||
setlocal
|
||||
|
||||
set ELECTRON_RUN_AS_NODE=1
|
||||
set ELECTRON_NO_ATTACH_CONSOLE=1
|
||||
|
||||
"%DESKTOP_PATH%" "%DESKTOP_ASKPASS_SCRIPT%" %*
|
||||
|
||||
endlocal
|
|
@ -130,18 +130,6 @@ export const renderer = merge({}, commonConfig, {
|
|||
],
|
||||
})
|
||||
|
||||
export const askPass = merge({}, commonConfig, {
|
||||
entry: { 'ask-pass': path.resolve(__dirname, 'src/ask-pass/main') },
|
||||
target: 'node',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin(
|
||||
Object.assign({}, replacements, {
|
||||
__PROCESS_KIND__: JSON.stringify('askpass'),
|
||||
})
|
||||
),
|
||||
],
|
||||
})
|
||||
|
||||
export const crash = merge({}, commonConfig, {
|
||||
entry: { crash: path.resolve(__dirname, 'src/crash/index') },
|
||||
target: 'electron-renderer',
|
||||
|
|
|
@ -9,7 +9,6 @@ const config: webpack.Configuration = {
|
|||
}
|
||||
|
||||
const mainConfig = merge({}, common.main, config)
|
||||
const askPassConfig = merge({}, common.askPass, config)
|
||||
const cliConfig = merge({}, common.cli, config)
|
||||
const highlighterConfig = merge({}, common.highlighter, config)
|
||||
|
||||
|
@ -80,7 +79,6 @@ const crashConfig = merge({}, common.crash, config, {
|
|||
export default [
|
||||
mainConfig,
|
||||
rendererConfig,
|
||||
askPassConfig,
|
||||
crashConfig,
|
||||
cliConfig,
|
||||
highlighterConfig,
|
||||
|
|
|
@ -11,7 +11,6 @@ const config: webpack.Configuration = {
|
|||
}
|
||||
|
||||
const mainConfig = merge({}, common.main, config)
|
||||
const askPassConfig = merge({}, common.askPass, config)
|
||||
const cliConfig = merge({}, common.cli, config)
|
||||
const highlighterConfig = merge({}, common.highlighter, config)
|
||||
|
||||
|
@ -65,7 +64,6 @@ const crashConfig = merge({}, common.crash, config, {
|
|||
export default [
|
||||
mainConfig,
|
||||
rendererConfig,
|
||||
askPassConfig,
|
||||
crashConfig,
|
||||
cliConfig,
|
||||
highlighterConfig,
|
||||
|
|
|
@ -311,9 +311,9 @@ delegates@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||
|
||||
desktop-trampoline@desktop/desktop-trampoline#v0.9.7:
|
||||
version "0.9.7"
|
||||
resolved "https://codeload.github.com/desktop/desktop-trampoline/tar.gz/38c590851f84e1dc856b6fd7e49920ef360c2ca0"
|
||||
desktop-trampoline@desktop/desktop-trampoline#v0.9.8:
|
||||
version "0.9.8"
|
||||
resolved "https://codeload.github.com/desktop/desktop-trampoline/tar.gz/cbd3dbb31d0d3ea9f325067f48bfbf60b6663a57"
|
||||
dependencies:
|
||||
node-addon-api "^3.1.0"
|
||||
prebuild-install "^6.0.0"
|
||||
|
|
|
@ -1,5 +1,24 @@
|
|||
{
|
||||
"releases": {
|
||||
"2.9.1-beta7": [
|
||||
"[Fixed] Wrong SSH key passphrases are not stored after multiple failed attempts and then one successful - #12804"
|
||||
],
|
||||
"2.9.1-beta6": [
|
||||
"[Fixed] Wrong SSH key passphrases are not stored - #12800",
|
||||
"[Fixed] Show SSH prompts (key passphrase, adding host, etc.) to macOS users via dialog - #12782"
|
||||
],
|
||||
"2.9.1-beta5": [
|
||||
"[Fixed] Fixed authentication errors in some Git operations - #12796"
|
||||
],
|
||||
"2.9.1-beta4": [
|
||||
"[Fixed] Show SSH prompts (key passphrase, adding host, etc.) to Windows users via dialog - #3457 #8761"
|
||||
],
|
||||
"2.9.1-beta3": [
|
||||
"[Added] Add support for IntelliJ CE for macOS - #12748. Thanks @T41US!",
|
||||
"[Fixed] Render links in commit messages when they are at the beginning of a line - #12105. Thanks @tsvetilian-ty!",
|
||||
"[Improved] Added support for more versions of JetBrains IDEs on Windows - #12778",
|
||||
"[Improved] Windows users can use the system OpenSSH for their Git repositories - #5641"
|
||||
],
|
||||
"2.9.1-beta2": [
|
||||
"[Added] Show number of lines changed in a commit - #11656",
|
||||
"[Improved] Increase visibility of misattributed commit warning in dark mode - #12210",
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
- [Enable Mandatory ASLR triggers cygheap errors](#enable-mandatory-aslr-triggers-cygheap-errors)
|
||||
- [I get a black screen when launching Desktop](#i-get-a-black-screen-when-launching-desktop)
|
||||
- [Failed to open CA file after an update](#failed-to-open-ca-file-after-an-update)
|
||||
- [`ask-pass-trampoline.bat` errors](#ask-pass-trampolinebat-errors)
|
||||
- [Authentication errors due to modified registry entries](#authentication-errors-due-to-modified-registry-entries)
|
||||
|
||||
# Known Issues
|
||||
|
@ -193,37 +192,6 @@ file:"C:\ProgramData/Git/config" http.sslcainfo=[some value here]
|
|||
sslCAInfo = [some value here]
|
||||
```
|
||||
|
||||
### `ask-pass-trampoline.bat` errors
|
||||
|
||||
Related issues: - [#2623](https://github.com/desktop/desktop/issues/2623), [#4124](https://github.com/desktop/desktop/issues/4124), [#6882](https://github.com/desktop/desktop/issues/6882), [#6789](https://github.com/desktop/desktop/issues/6879)
|
||||
|
||||
An example of the error message:
|
||||
|
||||
```
|
||||
The system cannot find the path specified.
|
||||
error: unable to read askpass response from 'C:\Users\User\AppData\Local\GitHubDesktop\app-1.6.2\resources\app\static\ask-pass-trampoline.bat'
|
||||
fatal: could not read Username for 'https://github.com': terminal prompts disabled"
|
||||
```
|
||||
|
||||
Known causes and workarounds:
|
||||
|
||||
**If you're experiencing this error, please download the [beta version](https://github.com/desktop/desktop#beta-channel) where it should hopefully be solved.**
|
||||
|
||||
- Modifying the `AutoRun` registry entry. To check if this entry has been modified open `Regedit.exe` and navigate to `HKEY_CURRENT_USER\Software\Microsoft\Command Processor\autorun` and `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Command Processor\autorun` to see if there is anything set (sometimes applications will also modify this). See [#6789](https://github.com/desktop/desktop/issues/6879#issuecomment-471042891) and [#2623](https://github.com/desktop/desktop/issues/2623#issuecomment-334305916) for examples of this.
|
||||
|
||||
- Special characters in your Windows username like a `&` or `-` can cause this error to be thrown. See [#7064](https://github.com/desktop/desktop/issues/7064) for an example of this. Try installing GitHub Desktop in a new user account to verify if this is the case.
|
||||
|
||||
- Antivirus software can sometimes prevent GitHub Desktop from installing correctly. If you are running antivirus software that could be causing this try temporarily disabling it and reinstalling GitHub Desktop.
|
||||
|
||||
- Restrictive permissions on your Windows user account. If you are running GitHub Desktop as a non-admin user try launching the application as an administrator (right-click -> `Run as administrator`). See [#5082](https://github.com/desktop/desktop/issues/5082#issuecomment-483067198).
|
||||
|
||||
- If none of these potential causes are present on your machine, try performing a fresh installation of GitHub Desktop to see if that gets things working again. Here are the steps you can take to do that:
|
||||
|
||||
1. Close GitHub Desktop
|
||||
2. Delete the `%AppData%\GitHub Desktop\` directory
|
||||
3. Delete the `%LocalAppData%\GitHubDesktop\` directory
|
||||
4. Reinstall GitHub Desktop from [desktop.github.com](https://desktop.github.com)
|
||||
|
||||
### Authentication errors due to modified registry entries
|
||||
|
||||
Related issue: [#2623](https://github.com/desktop/desktop/issues/2623)
|
||||
|
|
|
@ -101,14 +101,6 @@ initializes to perform asynchronous computation of syntax highlighting in diffs.
|
|||
Modules and logic that Desktop uses to show a default UI when an unhandled error
|
||||
occurs that crashes the main application.
|
||||
|
||||
### AskPass script - `app/src/ask-pass`
|
||||
|
||||
Modules and logic that Desktop uses to act as a credential helper for it's Git
|
||||
operations, bypassing whatever the user has set in config. This is a separate
|
||||
component because Desktop will spawn Git which can only spawn another program,
|
||||
so Desktop sets this script as the program to execute if Git encounters an
|
||||
authentication prompt.
|
||||
|
||||
### Command Line Interface - `app/src/cli`
|
||||
|
||||
Module and logic to be bundled for the `github` command line interface that
|
||||
|
|
|
@ -20,7 +20,6 @@ The webpack configuration files organize the source files into these targets:
|
|||
- `renderer.js` - logic in the renderer process in Electron
|
||||
- `crash.js` - specialised UI for displaying an error that crashed the app
|
||||
- `highlighter.js` - logic for syntax highlighting, which runs in a web worker
|
||||
- `ask-pass.js` - logic for handling authentication requests from Git
|
||||
- `cli.js` - logic for the `github` command line interface
|
||||
|
||||
Webpack also handles these steps:
|
||||
|
|
106
eslint-rules/set-almost-immediate.js
Normal file
106
eslint-rules/set-almost-immediate.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
// @ts-check
|
||||
|
||||
/**
|
||||
* set-almost-immediate
|
||||
*
|
||||
* This custom eslint rule is highly specific to GitHub Desktop and attempts
|
||||
* to prevent using setImmediate since, under some circumstances, could not work
|
||||
* at all in Electron 11.4.0+
|
||||
*
|
||||
* For more info about this issue, see: https://github.com/electron/electron/issues/29261
|
||||
*
|
||||
* As long as this issue persists, we'll use an alternative named setAlmostImmediate,
|
||||
* and this rule will ensure that's used instead of setImmediate.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('@typescript-eslint/experimental-utils').TSESLint.RuleModule} RuleModule
|
||||
* @typedef {import("@typescript-eslint/typescript-estree").TSESTree.TSTypeAnnotation} TSTypeAnnotation
|
||||
* @typedef {import("@typescript-eslint/typescript-estree").TSESTree.TypeNode} TypeNode
|
||||
*/
|
||||
|
||||
/** @type {RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
messages: {
|
||||
setImmediateForbidden: `setImmediate cannot be used, use setAlmostImmediate instead`,
|
||||
clearImmediateForbidden: `clearImmediate cannot be used, use clearAlmostImmediate instead`,
|
||||
nodeJSImmediateForbidden: `NodeJS.Immediate cannot be used, use AlmostImmediate instead`,
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
create: function (context) {
|
||||
const sourceCode = context.getSourceCode()
|
||||
|
||||
/**
|
||||
* Check if a type annotation contains any references to NodeJS.Immediate
|
||||
* and report them to be changed to AlmostImmediate.
|
||||
*
|
||||
* @param {TSTypeAnnotation} node
|
||||
* @param {TypeNode} typeAnnotation
|
||||
*/
|
||||
function scanTypeAnnotation(node, typeAnnotation) {
|
||||
if (typeAnnotation.type === 'TSTypeReference') {
|
||||
const typeName = sourceCode.getText(typeAnnotation)
|
||||
if (typeName === 'NodeJS.Immediate') {
|
||||
context.report({
|
||||
loc: typeAnnotation.loc,
|
||||
messageId: 'nodeJSImmediateForbidden',
|
||||
fix: fixer => {
|
||||
return fixer.replaceTextRange(
|
||||
typeAnnotation.range,
|
||||
'AlmostImmediate'
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if ('types' in typeAnnotation) {
|
||||
for (const type of typeAnnotation.types) {
|
||||
scanTypeAnnotation(node, type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
TSTypeAnnotation(node) {
|
||||
scanTypeAnnotation(node, node.typeAnnotation)
|
||||
},
|
||||
CallExpression(node) {
|
||||
const { callee } = node
|
||||
|
||||
if (callee.type !== 'Identifier') {
|
||||
return
|
||||
}
|
||||
|
||||
const functionName = sourceCode.getText(callee)
|
||||
|
||||
const offendingFunctions = {
|
||||
setImmediate: {
|
||||
messageId: 'setImmediateForbidden',
|
||||
replacement: 'setAlmostImmediate',
|
||||
},
|
||||
clearImmediate: {
|
||||
messageId: 'clearImmediateForbidden',
|
||||
replacement: 'clearAlmostImmediate',
|
||||
},
|
||||
}
|
||||
|
||||
const offendingCall = offendingFunctions[functionName]
|
||||
if (offendingCall !== undefined) {
|
||||
context.report({
|
||||
node: callee,
|
||||
messageId: offendingCall.messageId,
|
||||
fix: fixer => {
|
||||
return fixer.replaceTextRange(
|
||||
node.callee.range,
|
||||
offendingCall.replacement
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
80
eslint-rules/tests/set-almost-immediate.test.js
Normal file
80
eslint-rules/tests/set-almost-immediate.test.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
// @ts-check
|
||||
|
||||
const { ESLintUtils } = require('@typescript-eslint/experimental-utils')
|
||||
|
||||
const RuleTester = ESLintUtils.RuleTester
|
||||
const rule = require('../set-almost-immediate')
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: '@typescript-eslint/parser',
|
||||
})
|
||||
ruleTester.run('set-almost-immediate', rule, {
|
||||
valid: [
|
||||
{
|
||||
filename: 'app/src/ui/diff/helper.ts',
|
||||
code: `
|
||||
const ref: AlmostImmediate = setAlmostImmediate(() => {})
|
||||
clearAlmostImmediate(ref)
|
||||
`,
|
||||
},
|
||||
{
|
||||
filename: 'app/src/ui/some-class.ts',
|
||||
code: `
|
||||
class SomeClass {
|
||||
private ref: AlmostImmediate
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
filename: 'app/src/ui/helper.ts',
|
||||
code: `
|
||||
const ref: NodeJS.Immediate = setImmediate(() => {})
|
||||
clearImmediate(ref)
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'nodeJSImmediateForbidden',
|
||||
},
|
||||
{
|
||||
messageId: 'setImmediateForbidden',
|
||||
},
|
||||
{
|
||||
messageId: 'clearImmediateForbidden',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
const ref: AlmostImmediate = setAlmostImmediate(() => {})
|
||||
clearAlmostImmediate(ref)
|
||||
`,
|
||||
},
|
||||
{
|
||||
filename: 'app/src/ui/some-class.ts',
|
||||
code: `
|
||||
class SomeClass {
|
||||
private ref: NodeJS.Immediate
|
||||
private nullableRef: NodeJS.Immediate | null
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'nodeJSImmediateForbidden',
|
||||
},
|
||||
{
|
||||
messageId: 'nodeJSImmediateForbidden',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
class SomeClass {
|
||||
private ref: AlmostImmediate
|
||||
private nullableRef: AlmostImmediate | null
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
})
|
|
@ -344,6 +344,20 @@ function copyDependencies() {
|
|||
path.resolve(desktopTrampolineDir, desktopTrampolineFile)
|
||||
)
|
||||
|
||||
// Dev builds for macOS require a SSH wrapper to use SSH_ASKPASS
|
||||
if (process.platform === 'darwin' && isDevelopmentBuild) {
|
||||
console.log(' Copying ssh-wrapper')
|
||||
const sshWrapperFile = 'ssh-wrapper'
|
||||
fs.copySync(
|
||||
path.resolve(
|
||||
projectRoot,
|
||||
'app/node_modules/desktop-trampoline/build/Release',
|
||||
sshWrapperFile
|
||||
),
|
||||
path.resolve(desktopTrampolineDir, sshWrapperFile)
|
||||
)
|
||||
}
|
||||
|
||||
console.log(' Copying git environment…')
|
||||
const gitDir = path.resolve(outRoot, 'git')
|
||||
fs.removeSync(gitDir)
|
||||
|
|
Loading…
Reference in a new issue