Merge branch 'development' into rebase-multi-comit-op

This commit is contained in:
tidy-dev 2021-08-19 11:47:58 -04:00
commit f607b4a40f
44 changed files with 634 additions and 331 deletions

View file

@ -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 #

View file

@ -13,7 +13,6 @@ module.exports = {
'!**/vendor/**',
'!**/*.d.*',
// not focused on testing these areas currently
'!src/ask-pass/**/*',
'!src/cli/**/*',
'!src/crash/**/*',
'!src/highlighter/**/*',

View file

@ -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",

View file

@ -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
}

View file

@ -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()
}
})

View file

@ -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

View file

@ -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()
}

View file

@ -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]),
})

View file

@ -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)
})
}
/**

View file

@ -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)

View file

@ -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

View file

@ -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

View 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)
}
}

View 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
)
}

View file

@ -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)
}
}

View file

@ -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()
})

View file

@ -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))

View file

@ -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')

View file

@ -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,
}

View file

@ -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.
*

View file

@ -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')
}

View file

@ -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')
}

View file

@ -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 }),
})
})
}

View file

@ -303,5 +303,8 @@ export type Popup =
| {
type: PopupType.SSHKeyPassphrase
keyPath: string
onSubmit: (passphrase: string | undefined) => void
onSubmit: (
passphrase: string | undefined,
storePassphrase: boolean
) => void
}

View file

@ -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!')
})
}

View file

@ -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

View file

@ -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)
}
}
})

View file

@ -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
}

View file

@ -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

View file

@ -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)
}
}

View file

@ -1,3 +0,0 @@
#!/bin/sh
ELECTRON_RUN_AS_NODE=1 "$DESKTOP_PATH" "$DESKTOP_ASKPASS_SCRIPT" "$@"

View file

@ -1,3 +0,0 @@
#!/bin/sh
ELECTRON_RUN_AS_NODE=1 "$DESKTOP_PATH" "$DESKTOP_ASKPASS_SCRIPT" "$@"

View file

@ -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

View file

@ -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',

View file

@ -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,

View file

@ -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,

View file

@ -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"

View file

@ -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",

View file

@ -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)

View file

@ -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

View file

@ -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:

View 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
)
},
})
}
},
}
},
}

View 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
}
`,
},
],
})

View file

@ -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)