GitInvocation.ps1
|
function Invoke-GitProcess { <# .SYNOPSIS Capture a non-interactive git process without changing the parent environment. .DESCRIPTION Low-level runner for repository discovery and callers that own exit handling. Arguments are individual tokens, passed directly to the native git executable. StandardOutput and StandardError preserve the decoded text, including newlines. Output preserves the legacy stdout-then-stderr line array (not chronological interleaving), including a trailing empty line for each newline-ended stream. Non-zero exits are returned, not written as errors. Launch failures terminate. .PARAMETER Environment Child-only environment overrides. A null value removes an inherited variable. Credential prompts, pagers and interactive editors are disabled. #> [OutputType('GitInvocationResult')] [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNull()] [ValidateCount(1, 2147483647)] [AllowEmptyString()] [ValidateScript({ -not $_.Contains([char]0) }, ErrorMessage = 'Git arguments must not contain NUL characters.')] [string[]]$Arguments, [hashtable]$Environment ) # Resolve an application explicitly; never dispatch a function, alias or # cmd/bat shim, whose shell would reinterpret otherwise literal arguments. $executableName = if ($IsWindows) { 'git.exe' } else { 'git' } $executable = Get-Command $executableName -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $executable) { $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new( [System.IO.FileNotFoundException]::new('The native git executable was not found on PATH.'), 'GitExecutableNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $executableName)) } $psi = [System.Diagnostics.ProcessStartInfo]::new() $psi.FileName = $executable.Source $psi.WorkingDirectory = $ExecutionContext.SessionState.Path.CurrentFileSystemLocation.ProviderPath foreach ($argument in $Arguments) { $psi.ArgumentList.Add($argument) } $psi.UseShellExecute = $false $psi.CreateNoWindow = $true $psi.RedirectStandardInput = $true $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.StandardOutputEncoding = [System.Text.UTF8Encoding]::new($false) $psi.StandardErrorEncoding = [System.Text.UTF8Encoding]::new($false) if ($Environment) { foreach ($key in $Environment.Keys) { if ($null -eq $Environment[$key]) { $null = $psi.Environment.Remove($key) } else { $psi.Environment[$key] = [string]$Environment[$key] } } } $psi.Environment['GIT_TERMINAL_PROMPT'] = '0' $psi.Environment['GCM_INTERACTIVE'] = 'never' $psi.Environment['GIT_ASKPASS'] = 'false' $psi.Environment['SSH_ASKPASS'] = 'false' $psi.Environment['GIT_PAGER'] = 'cat' $psi.Environment['GIT_EDITOR'] = 'false' $psi.Environment['GIT_SEQUENCE_EDITOR'] = 'false' $proc = [System.Diagnostics.Process]::new() try { $proc.StartInfo = $psi $null = $proc.Start() $proc.StandardInput.Close() # Both reads must be in flight before waiting: either pipe can fill. $stdoutTask = $proc.StandardOutput.ReadToEndAsync() $stderrTask = $proc.StandardError.ReadToEndAsync() $proc.WaitForExit() $stdoutText = $stdoutTask.GetAwaiter().GetResult() $stderrText = $stderrTask.GetAwaiter().GetResult() $exitCode = $proc.ExitCode } catch [System.ComponentModel.Win32Exception], [System.InvalidOperationException] { $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new( $_.Exception, 'GitProcessFailed', [System.Management.Automation.ErrorCategory]::ResourceUnavailable, $executable.Source)) } finally { $proc.Dispose() } $lines = [System.Collections.Generic.List[string]]::new() foreach ($block in @($stdoutText, $stderrText)) { if ($block) { foreach ($line in ($block -split "\r?\n")) { $lines.Add($line) } } } [PSCustomObject]@{ PSTypeName = 'GitInvocationResult' ExitCode = $exitCode StandardOutput = $stdoutText StandardError = $stderrText Output = $lines.ToArray() } } function Invoke-Git { <# .SYNOPSIS Run git in a resolved repository and report native failures as PowerShell errors. .DESCRIPTION Private entry point for module commands. Does not change location or LASTEXITCODE. The caller owns ShouldProcess for mutations and command-specific validation of refs/options; use -- where git accepts it before operand values. Arguments must not be a pre-quoted command string. .PARAMETER Path Literal directory inside a working tree. Defaults to the current location. Resolved by Resolve-GitRepositoryPath and passed as a separate -C argument. .PARAMETER AllowBare Allow targeting a bare repository. .PARAMETER AllowNonZeroExit Return the result without an error when git exits non-zero, for commands whose exit codes carry domain meaning. Otherwise emit GitCommandFailed and no result; -ErrorAction Stop promotes that error to terminating. The error's TargetObject contains the result, including both streams, ExitCode and RepositoryPath. .PARAMETER Environment Child-only environment overrides, as in Invoke-GitProcess. #> [OutputType('GitInvocationResult')] [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNull()] [ValidateCount(1, 2147483647)] [AllowEmptyString()] [ValidateScript({ -not $_.Contains([char]0) }, ErrorMessage = 'Git arguments must not contain NUL characters.')] [string[]]$Arguments, [Alias('RepositoryPath', 'RepoPath')] [string]$Path, [hashtable]$Environment, [switch]$AllowBare, [switch]$AllowNonZeroExit ) $repositoryPath = Resolve-GitRepositoryPath -Path $Path -AllowBare:$AllowBare -Environment $Environment if (-not $repositoryPath) { return } $result = Invoke-GitProcess -Arguments (@('-C', $repositoryPath) + $Arguments) -Environment $Environment $result | Add-Member -NotePropertyName RepositoryPath -NotePropertyValue $repositoryPath if ($result.ExitCode -ne 0 -and -not $AllowNonZeroExit) { $detail = if ($result.StandardError) { $result.StandardError.TrimEnd() } elseif ($result.StandardOutput) { $result.StandardOutput.TrimEnd() } else { 'No output.' } $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new("git failed in '$repositoryPath' (exit $($result.ExitCode)): $detail"), 'GitCommandFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $result) $PSCmdlet.WriteError($errorRecord) return } $result } |