@@ -9,7 +9,10 @@ import * as path from 'path'
99
1010import {
1111 findWindowsBash ,
12+ isGitBashAbsolutePath ,
1213 parseRegistryPath ,
14+ resetTranslationCache ,
15+ translateGitBashPath ,
1316 windowsBashCandidates ,
1417} from '../tools/windows-bash'
1518
@@ -222,3 +225,175 @@ describe('parseRegistryPath', () => {
222225 expect ( parseRegistryPath ( ' InstallPath REG_SZ ' , 'InstallPath' ) ) . toBeNull ( )
223226 } )
224227} )
228+
229+ describe ( 'Git Bash path translation' , ( ) => {
230+ beforeEach ( ( ) => {
231+ resetTranslationCache ( )
232+ } )
233+
234+ it ( 'recognizes MSYS absolute paths without claiming native UNC paths' , ( ) => {
235+ expect ( isGitBashAbsolutePath ( '/tmp/file.txt' ) ) . toBe ( true )
236+ expect ( isGitBashAbsolutePath ( '/c/Users/Alice/file.txt' ) ) . toBe ( true )
237+ expect ( isGitBashAbsolutePath ( '//server/share/file.txt' ) ) . toBe ( false )
238+ expect ( isGitBashAbsolutePath ( 'C:\\tmp\\file.txt' ) ) . toBe ( false )
239+ expect ( isGitBashAbsolutePath ( 'src/file.txt' ) ) . toBe ( false )
240+ } )
241+
242+ it ( 'translates a drive-letter MSYS path without any lookup or spawn' , ( ) => {
243+ const neverFindBash = ( ) => {
244+ throw new Error ( 'must not look for bash' )
245+ }
246+ expect (
247+ translateGitBashPath ( '/c/Users/me/repo/src/a.ts' , {
248+ platform : 'win32' ,
249+ env : { } ,
250+ findBash : neverFindBash ,
251+ } ) ,
252+ ) . toBe ( 'C:\\Users\\me\\repo\\src\\a.ts' )
253+ expect (
254+ translateGitBashPath ( '/D/work/file.txt' , {
255+ platform : 'win32' ,
256+ env : { } ,
257+ findBash : neverFindBash ,
258+ } ) ,
259+ ) . toBe ( 'D:\\work\\file.txt' )
260+ } )
261+
262+ it ( 'spawns the cygpath.exe derived from the found bash, directly' , ( ) => {
263+ const bash = path . join ( installGitAt ( 'Git' ) , 'bin' , 'bash.exe' )
264+ const cygpath = touch ( path . join ( root , 'Git' , 'usr' , 'bin' ) , 'cygpath.exe' )
265+ const calls : [ string , string ] [ ] = [ ]
266+
267+ expect (
268+ translateGitBashPath ( '/tmp/file.txt' , {
269+ platform : 'win32' ,
270+ env : { } ,
271+ findBash : ( ) => bash ,
272+ spawnCygpath : ( cygpath , filePath ) => {
273+ calls . push ( [ cygpath , filePath ] )
274+ return {
275+ status : 0 ,
276+ stdout : 'C:\\Users\\Alice\\AppData\\Local\\Temp\\file.txt\n' ,
277+ }
278+ } ,
279+ } ) ,
280+ ) . toBe ( 'C:\\Users\\Alice\\AppData\\Local\\Temp\\file.txt' )
281+ expect ( calls ) . toEqual ( [ [ cygpath , '/tmp/file.txt' ] ] )
282+ } )
283+
284+ it ( 'spawns the cygpath.exe sitting beside a usr\\bin bash' , ( ) => {
285+ const bash = touch ( path . join ( root , 'Git' , 'usr' , 'bin' ) , 'bash.exe' )
286+ const cygpath = touch ( path . join ( root , 'Git' , 'usr' , 'bin' ) , 'cygpath.exe' )
287+ const calls : [ string , string ] [ ] = [ ]
288+
289+ expect (
290+ translateGitBashPath ( '/tmp/sibling.txt' , {
291+ platform : 'win32' ,
292+ env : { } ,
293+ findBash : ( ) => bash ,
294+ spawnCygpath : ( cygpath , filePath ) => {
295+ calls . push ( [ cygpath , filePath ] )
296+ return { status : 0 , stdout : 'C:\\Temp\\sibling.txt\n' }
297+ } ,
298+ } ) ,
299+ ) . toBe ( 'C:\\Temp\\sibling.txt' )
300+ expect ( calls ) . toEqual ( [ [ cygpath , '/tmp/sibling.txt' ] ] )
301+ } )
302+
303+ it ( 'skips the spawn and keeps the original path when no cygpath.exe exists' , ( ) => {
304+ const bash = touch ( path . join ( root , 'Git' , 'bin' ) , 'bash.exe' )
305+ let spawns = 0
306+
307+ expect (
308+ translateGitBashPath ( '/tmp/no-cygpath.txt' , {
309+ platform : 'win32' ,
310+ env : { } ,
311+ findBash : ( ) => bash ,
312+ spawnCygpath : ( ) => {
313+ spawns ++
314+ return { status : 0 , stdout : 'C:\\Temp\\no-cygpath.txt\n' }
315+ } ,
316+ } ) ,
317+ ) . toBe ( '/tmp/no-cygpath.txt' )
318+ expect ( spawns ) . toBe ( 0 )
319+ } )
320+
321+ it ( 'reads only the last stdout line, so leading banner output is ignored' , ( ) => {
322+ expect (
323+ translateGitBashPath ( '/tmp/banner.txt' , {
324+ platform : 'win32' ,
325+ env : { } ,
326+ findBash : ( ) => path . join ( root , 'Git' , 'bin' , 'bash.exe' ) ,
327+ pathExists : ( ) => true ,
328+ spawnCygpath : ( ) => ( {
329+ status : 0 ,
330+ stdout :
331+ 'conda environment activated\nNow using node v22.1.0\nC:\\Temp\\banner.txt\r\n' ,
332+ } ) ,
333+ } ) ,
334+ ) . toBe ( 'C:\\Temp\\banner.txt' )
335+ } )
336+
337+ it ( 'falls back to the original path on a non-zero exit even when stdout is non-empty' , ( ) => {
338+ expect (
339+ translateGitBashPath ( '/tmp/exit-code.txt' , {
340+ platform : 'win32' ,
341+ env : { } ,
342+ findBash : ( ) => path . join ( root , 'Git' , 'bin' , 'bash.exe' ) ,
343+ pathExists : ( ) => true ,
344+ spawnCygpath : ( ) => ( {
345+ status : 1 ,
346+ stdout : 'C:\\Temp\\exit-code.txt\n' ,
347+ } ) ,
348+ } ) ,
349+ ) . toBe ( '/tmp/exit-code.txt' )
350+ } )
351+
352+ it ( 'spawns once per path and answers repeats from the cache' , ( ) => {
353+ let spawns = 0
354+ const dependencies = {
355+ platform : 'win32' as NodeJS . Platform ,
356+ env : { } ,
357+ findBash : ( ) => path . join ( root , 'Git' , 'bin' , 'bash.exe' ) ,
358+ pathExists : ( ) => true ,
359+ spawnCygpath : ( ) => {
360+ spawns ++
361+ return { status : 0 , stdout : 'C:\\Temp\\repeat.txt\n' }
362+ } ,
363+ }
364+ expect ( translateGitBashPath ( '/tmp/repeat.txt' , dependencies ) ) . toBe (
365+ 'C:\\Temp\\repeat.txt' ,
366+ )
367+ expect ( translateGitBashPath ( '/tmp/repeat.txt' , dependencies ) ) . toBe (
368+ 'C:\\Temp\\repeat.txt' ,
369+ )
370+ expect ( spawns ) . toBe ( 1 )
371+ } )
372+
373+ it ( 'leaves paths unchanged when translation does not apply or is unavailable' , ( ) => {
374+ const neverFindBash = ( ) => {
375+ throw new Error ( 'must not look for bash' )
376+ }
377+ expect (
378+ translateGitBashPath ( '/tmp/file.txt' , {
379+ platform : 'darwin' ,
380+ findBash : neverFindBash ,
381+ } ) ,
382+ ) . toBe ( '/tmp/file.txt' )
383+ expect (
384+ translateGitBashPath ( 'C:\\tmp\\file.txt' , {
385+ platform : 'win32' ,
386+ findBash : neverFindBash ,
387+ } ) ,
388+ ) . toBe ( 'C:\\tmp\\file.txt' )
389+ expect (
390+ translateGitBashPath ( '/tmp/file.txt' , {
391+ platform : 'win32' ,
392+ env : { } ,
393+ findBash : ( ) => path . join ( root , 'Git' , 'bin' , 'bash.exe' ) ,
394+ pathExists : ( ) => true ,
395+ spawnCygpath : ( ) => ( { status : 0 , stdout : '' } ) ,
396+ } ) ,
397+ ) . toBe ( '/tmp/file.txt' )
398+ } )
399+ } )
0 commit comments