Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions apps/desktop/electron/find-git-bash.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import assert from 'node:assert/strict'

import { test } from 'vitest'

import { findGitBash } from './find-git-bash'

const yes = () => true
const no = () => false

test('HERMES_GIT_BASH_PATH override takes precedence', () => {
const result = findGitBash({
isWindows: true,
env: { HERMES_GIT_BASH_PATH: 'D:\\CustomGit\\bin\\bash.exe' },
fileExists: yes,
findOnPath: () => null
})

assert.equal(result, 'D:\\CustomGit\\bin\\bash.exe')
})

test('HERMES_GIT_BASH_PATH invalid path falls through to candidates', () => {
const env = {
HERMES_GIT_BASH_PATH: 'X:\\Missing\\bash.exe',
LOCALAPPDATA: 'C:\\Users\\test\\AppData\\Local',
ProgramFiles: 'C:\\Program Files',
'ProgramFiles(x86)': 'C:\\Program Files (x86)'
}

const fileExists = (p: string) => p !== 'X:\\Missing\\bash.exe' && p.includes('Program Files\\Git\\bin\\bash.exe')
const result = findGitBash({ isWindows: true, env, fileExists, findOnPath: () => null })
assert.equal(result, 'C:\\Program Files\\Git\\bin\\bash.exe')
})

test('HERMES_GIT_BASH_PATH empty string is ignored', () => {
const result = findGitBash({
isWindows: true,
env: { HERMES_GIT_BASH_PATH: '', LOCALAPPDATA: '' },
fileExists: no,
findOnPath: () => 'C:\\msys64\\usr\\bin\\bash.exe'
})

assert.equal(result, 'C:\\msys64\\usr\\bin\\bash.exe')
})

test('non-Windows uses findOnPath', () => {
const result = findGitBash({
isWindows: false,
env: {},
fileExists: no,
findOnPath: () => '/usr/bin/bash'
})

assert.equal(result, '/usr/bin/bash')
})
67 changes: 67 additions & 0 deletions apps/desktop/electron/find-git-bash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import path from 'node:path'

export interface GitBashOptions {
isWindows: boolean
env: Record<string, string | undefined>
fileExists: (filePath: string) => boolean
findOnPath?: (command: string) => string | null
}

/**
* Locate bash.exe on Windows.
* Resolution order (first match wins):
* 1. HERMES_GIT_BASH_PATH env var override
* 2. PortableGit under %LOCALAPPDATA%\hermes\git\ (install.ps1)
* 3. Standard Git for Windows install locations
* 4. %LOCALAPPDATA%\Programs\Git\ (user-scoped)
* 5. bash on PATH
*/
export function findGitBash(opts: GitBashOptions): string | null {
const { isWindows, env, fileExists, findOnPath } = opts

if (!isWindows) {
return findOnPath ? findOnPath('bash') : null
}

// Respect HERMES_GIT_BASH_PATH if set (mirrors tools/environments/local.py:_find_bash).
const gitBashPath = env.HERMES_GIT_BASH_PATH

if (gitBashPath && fileExists(gitBashPath)) {
return gitBashPath
}

const localAppData = env.LOCALAPPDATA || ''
const candidates: string[] = []

// Candidate paths are Windows paths regardless of host platform (tests run
// on POSIX CI hosts too), so join with win32 semantics explicitly.
const joinWin = path.win32.join

if (localAppData) {
candidates.push(joinWin(localAppData, 'hermes', 'git', 'bin', 'bash.exe'))
candidates.push(joinWin(localAppData, 'hermes', 'git', 'usr', 'bin', 'bash.exe'))
}

candidates.push(joinWin(env['ProgramFiles'] || 'C:\\Program Files', 'Git', 'bin', 'bash.exe'))
candidates.push(joinWin(env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Git', 'bin', 'bash.exe'))

if (localAppData) {
candidates.push(joinWin(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'))
}

for (const candidate of candidates) {
if (fileExists(candidate)) {
return candidate
}
}

if (findOnPath) {
const onPath = findOnPath('bash')

if (onPath) {
return onPath
}
}

return null
}
51 changes: 10 additions & 41 deletions apps/desktop/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
} from './desktop-uninstall'
import { installEmbedReferer } from './embed-referer'
import { createEventDeduper } from './event-dedupe'
import { findGitBash as _findGitBash } from './find-git-bash'
import { readDirForIpc } from './fs-read-dir'
import { probeGatewayWebSocket } from './gateway-ws-probe'
import { scanGitRepos } from './git-repo-scan'
Expand Down Expand Up @@ -1915,48 +1916,16 @@ function findSystemPython() {
return null
}

// findGitBash — locate bash.exe on Windows. Hermes' terminal tool requires
// bash (POSIX shell), and on Windows that's almost always Git for Windows'
// bundled Git Bash. We check the same set of locations tools/environments/
// local.py:_find_bash() checks at runtime, so a positive result here means
// the agent will be able to start a terminal too.
//
// On non-Windows hosts bash is part of the OS and this just returns the
// first bash on PATH.
// findGitBash — locate bash.exe on Windows. Resolves HERMES_GIT_BASH_PATH
// first (mirrors tools/environments/local.py:_find_bash), then PortableGit,
// standard install locations, and finally PATH.
function findGitBash() {
if (!IS_WINDOWS) {
return findOnPath('bash')
}

// install.ps1 drops PortableGit at %LOCALAPPDATA%\hermes\git\... — checked
// first so users who installed via install.ps1 are detected before we
// start probing system-wide locations.
const localAppData = process.env.LOCALAPPDATA || ''
const candidates = []

if (localAppData) {
candidates.push(path.join(localAppData, 'hermes', 'git', 'bin', 'bash.exe'))
candidates.push(path.join(localAppData, 'hermes', 'git', 'usr', 'bin', 'bash.exe'))
}

// Standard Git for Windows install locations.
candidates.push(path.join(process.env['ProgramFiles'] || 'C:\\Program Files', 'Git', 'bin', 'bash.exe'))
candidates.push(path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Git', 'bin', 'bash.exe'))

if (localAppData) {
candidates.push(path.join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'))
}

for (const candidate of candidates) {
if (fileExists(candidate)) {
return candidate
}
}

// Last resort — bash on PATH (covers WSL bash, MSYS2, custom installs).
// On WSL hosts findOnPath itself filters out Windows-binary paths via
// isWindowsBinaryPathInWsl, so we won't hand back a wsl.exe shim either.
return findOnPath('bash')
return _findGitBash({
isWindows: IS_WINDOWS,
env: process.env,
fileExists,
findOnPath
})
}

function getVenvPython(venvRoot) {
Expand Down
2 changes: 2 additions & 0 deletions contributors/emails/xiehong@xinjikang.cn
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
seamusmore
# PR #64339 salvage
Loading