diff --git a/lib/overcommit/utils.rb b/lib/overcommit/utils.rb index 9e9d045e..ef56537a 100644 --- a/lib/overcommit/utils.rb +++ b/lib/overcommit/utils.rb @@ -293,7 +293,29 @@ def windows_parent_command native_windows_parent_command rescue LoadError, StandardError # win32ole is optional and absent from newer Ruby distributions. - # Fall back when it is unavailable or the COM query is denied. + # Use Script Host when it is unavailable or the COM query is denied. + scripted_windows_parent_command + end + + def scripted_windows_parent_command + begin + result = execute([ + 'cscript.exe', '//nologo', '//B', '//U', + File.expand_path('../../libexec/overcommit-parent-command.js', __dir__), + Process.ppid.to_s, + ]) + if result.success? + command = result.stdout.dup.force_encoding('UTF-16LE').encode('UTF-8'). + delete_prefix("\uFEFF").strip + return command.empty? ? nil : command + end + rescue Errno::ENOENT, ChildProcess::LaunchError, EncodingError + # Preserve PowerShell when Script Host is disabled or unavailable. + end + powershell_windows_parent_command + end + + def powershell_windows_parent_command result = execute([ 'powershell.exe', '-NoProfile', '-NonInteractive', '-Command', "(Get-CimInstance Win32_Process -Filter 'ProcessId = #{Process.ppid}').CommandLine", diff --git a/libexec/overcommit-parent-command.js b/libexec/overcommit-parent-command.js new file mode 100644 index 00000000..6babb9b5 --- /dev/null +++ b/libexec/overcommit-parent-command.js @@ -0,0 +1,11 @@ +// Query the same WMI property as PowerShell without its startup overhead. +// cscript //U writes redirected output as UTF-16LE. +var pid = String(WScript.Arguments(0)); +if (!/^\d+$/.test(pid)) WScript.Quit(2); +var service = GetObject('winmgmts:root/cimv2'); +var processes = service.ExecQuery('SELECT CommandLine FROM Win32_Process WHERE ProcessId = ' + pid); +var items = new Enumerator(processes); +for (; !items.atEnd(); items.moveNext()) { + var command = items.item().CommandLine; + if (command !== null) WScript.StdOut.WriteLine(command); +} diff --git a/spec/overcommit/utils_spec.rb b/spec/overcommit/utils_spec.rb index b69dafe2..10d8c751 100644 --- a/spec/overcommit/utils_spec.rb +++ b/spec/overcommit/utils_spec.rb @@ -219,6 +219,72 @@ end end + context 'when Windows falls back to Script Host' do + let(:command) { 'git -C "C:/Users/José/项目" commit --amend' } + let(:output) { "\uFEFF#{command}\r\n".encode('UTF-16LE') } + let(:script_result) { Overcommit::Subprocess::Result.new(0, output, '') } + + before do + Overcommit::OS.stub(:windows?).and_return(true) + described_class.stub(:require).with('win32ole').and_raise(LoadError) + described_class.stub(:execute).with(array_including('cscript.exe')). + and_return(script_result) + end + + it 'decodes the complete Unicode command line and removes its BOM' do + described_class.should_not_receive(:execute).with(array_including('powershell.exe')) + subject.should == command + end + + context 'when the process has exited' do + let(:output) { '' } + + it 'returns nil without retrying PowerShell' do + described_class.should_not_receive(:execute).with(array_including('powershell.exe')) + subject.should be_nil + end + end + + context 'when the script fails' do + let(:script_result) { Overcommit::Subprocess::Result.new(1, '', 'Access denied') } + + before do + described_class.stub(:execute).with(array_including('powershell.exe')). + and_return(Overcommit::Subprocess::Result.new(0, "#{command}\n", '')) + end + + it 'falls back to PowerShell' do + subject.should == command + end + end + + context 'when Script Host is unavailable' do + before do + described_class.stub(:execute).with(array_including('cscript.exe')). + and_raise(Errno::ENOENT) + described_class.stub(:execute).with(array_including('powershell.exe')). + and_return(Overcommit::Subprocess::Result.new(0, "#{command}\n", '')) + end + + it 'falls back to PowerShell' do + subject.should == command + end + end + + context 'when Script Host returns invalid Unicode' do + let(:output) { "\x00".b } + + before do + described_class.stub(:execute).with(array_including('powershell.exe')). + and_return(Overcommit::Subprocess::Result.new(0, "#{command}\n", '')) + end + + it 'falls back to PowerShell' do + subject.should == command + end + end + end + context 'when there is no parent' do before do Process.stub(:ppid) { 0 }