@@ -0,0 +1,291 @@
|
||||
# Shared helpers for git-init skill scripts.
|
||||
# Dot-source only. Never prints token values.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) {
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
}
|
||||
|
||||
|
||||
# Windows PowerShell 5.1 promotes native stderr to a terminating error when
|
||||
# $ErrorActionPreference='Stop', so 2>$null alone is not enough. This helper
|
||||
# runs a native executable with stderr discarded and $LASTEXITCODE preserved.
|
||||
function Invoke-GitSkillsNative {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
[string[]]$ArgumentList = @()
|
||||
)
|
||||
$prevEap = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
$output = @(& $FilePath @ArgumentList 2>$null | ForEach-Object { "$_" })
|
||||
return ,$output
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $prevEap
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-GitSkillsNativeWithInput {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
[string[]]$ArgumentList = @(),
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$InputText
|
||||
)
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $FilePath
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.RedirectStandardOutput = $true
|
||||
$psi.RedirectStandardError = $true
|
||||
$psi.CreateNoWindow = $true
|
||||
$psi.Arguments = ($ArgumentList | ForEach-Object {
|
||||
if ($_ -match '\s|"') { '"{0}"' -f ($_ -replace '"', '\"') } else { $_ }
|
||||
}) -join ' '
|
||||
|
||||
$proc = New-Object System.Diagnostics.Process
|
||||
$proc.StartInfo = $psi
|
||||
[void]$proc.Start()
|
||||
foreach ($line in ($InputText -split "`r?`n", 0)) {
|
||||
$proc.StandardInput.WriteLine($line)
|
||||
}
|
||||
$proc.StandardInput.Close()
|
||||
$stdout = $proc.StandardOutput.ReadToEnd()
|
||||
$stderr = $proc.StandardError.ReadToEnd()
|
||||
$proc.WaitForExit()
|
||||
$script:LastNativeExitCode = $proc.ExitCode
|
||||
|
||||
if ($stderr) { $null = $stderr }
|
||||
if ([string]::IsNullOrWhiteSpace($stdout)) { return @() }
|
||||
return @($stdout -split "`r?`n")
|
||||
}
|
||||
|
||||
Add-Type -AssemblyName System.Security | Out-Null
|
||||
|
||||
$script:GitSkillsHome = Join-Path $env:USERPROFILE '.git-skills'
|
||||
$script:ProfilePath = Join-Path $script:GitSkillsHome 'profile.json'
|
||||
$script:TokenPath = Join-Path $script:GitSkillsHome 'token.dpapi'
|
||||
$script:DefaultsPath = Join-Path $PSScriptRoot 'defaults.json'
|
||||
|
||||
function Get-GitSkillsDefaults {
|
||||
if (-not (Test-Path -LiteralPath $script:DefaultsPath)) {
|
||||
return [pscustomobject]@{
|
||||
userName = 'Traveler'
|
||||
userEmail = 'user@example.com'
|
||||
}
|
||||
}
|
||||
return Get-Content -LiteralPath $script:DefaultsPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Ensure-GitSkillsHome {
|
||||
if (-not (Test-Path -LiteralPath $script:GitSkillsHome)) {
|
||||
New-Item -ItemType Directory -Path $script:GitSkillsHome -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-GitSkillsProfile {
|
||||
if (-not (Test-Path -LiteralPath $script:ProfilePath)) {
|
||||
return $null
|
||||
}
|
||||
return Get-Content -LiteralPath $script:ProfilePath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Save-GitSkillsProfile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Profile
|
||||
)
|
||||
Ensure-GitSkillsHome
|
||||
$json = $Profile | ConvertTo-Json -Depth 5
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding $true
|
||||
[System.IO.File]::WriteAllText($script:ProfilePath, $json, $utf8Bom)
|
||||
}
|
||||
|
||||
function Protect-SecretString {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PlainText
|
||||
)
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($PlainText)
|
||||
return [System.Security.Cryptography.ProtectedData]::Protect(
|
||||
$bytes,
|
||||
$null,
|
||||
[System.Security.Cryptography.DataProtectionScope]::CurrentUser
|
||||
)
|
||||
}
|
||||
|
||||
function Unprotect-SecretBytes {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[byte[]]$Data
|
||||
)
|
||||
$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect(
|
||||
$Data,
|
||||
$null,
|
||||
[System.Security.Cryptography.DataProtectionScope]::CurrentUser
|
||||
)
|
||||
return [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||
}
|
||||
|
||||
function ConvertFrom-SecureStringPlain {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Security.SecureString]$Secure
|
||||
)
|
||||
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secure)
|
||||
try {
|
||||
return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
|
||||
}
|
||||
finally {
|
||||
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
||||
}
|
||||
}
|
||||
|
||||
function Parse-GitRemoteUrl {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RemoteUrl
|
||||
)
|
||||
$url = $RemoteUrl.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($url)) {
|
||||
throw 'Remote URL is empty.'
|
||||
}
|
||||
|
||||
function New-ParsedRemote {
|
||||
param(
|
||||
[string]$Protocol,
|
||||
[string]$HostName,
|
||||
[object]$Port,
|
||||
[string]$Path
|
||||
)
|
||||
$portNum = $null
|
||||
if ($null -ne $Port -and "$Port" -ne '') {
|
||||
$portNum = [int]$Port
|
||||
}
|
||||
|
||||
$credentialHost = $HostName
|
||||
if ($Protocol -eq 'https' -or $Protocol -eq 'http') {
|
||||
$defaultPort = if ($Protocol -eq 'https') { 443 } else { 80 }
|
||||
if ($null -ne $portNum -and $portNum -ne $defaultPort) {
|
||||
$credentialHost = '{0}:{1}' -f $HostName, $portNum
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
Protocol = $Protocol
|
||||
Host = $HostName
|
||||
Port = $portNum
|
||||
Path = $Path
|
||||
CredentialHost = $credentialHost
|
||||
}
|
||||
}
|
||||
|
||||
# Gitea / Git SCP: git@host:owner/repo.git
|
||||
if ($url -match '^git@([^:]+):(.+)$') {
|
||||
return New-ParsedRemote -Protocol 'ssh' -HostName $Matches[1] -Port $null -Path $Matches[2]
|
||||
}
|
||||
|
||||
# ssh://git@host:2222/owner/repo.git or ssh://git@host/owner/repo.git
|
||||
if ($url -match '^ssh://(?:[^@/]+@)?(\[[^\]]+\]|[^:/]+)(?::(\d+))?/(.+)$') {
|
||||
$port = if ($Matches[2]) { [int]$Matches[2] } else { $null }
|
||||
return New-ParsedRemote -Protocol 'ssh' -HostName $Matches[1].Trim('[]') -Port $port -Path $Matches[3]
|
||||
}
|
||||
|
||||
try {
|
||||
$uri = [Uri]$url
|
||||
}
|
||||
catch {
|
||||
throw "Unsupported remote URL: $url"
|
||||
}
|
||||
|
||||
if (-not $uri.IsAbsoluteUri -or [string]::IsNullOrWhiteSpace($uri.Host)) {
|
||||
throw "Unsupported remote URL: $url"
|
||||
}
|
||||
|
||||
$port = $null
|
||||
if (-not $uri.IsDefaultPort) {
|
||||
$port = $uri.Port
|
||||
}
|
||||
|
||||
return New-ParsedRemote -Protocol $uri.Scheme -HostName $uri.Host -Port $port -Path $uri.AbsolutePath.TrimStart('/')
|
||||
}
|
||||
|
||||
function Write-StatusLine {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Key,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Value
|
||||
)
|
||||
Write-Output ("{0}={1}" -f $Key, $Value)
|
||||
}
|
||||
|
||||
function Get-CoAuthorTrailerLines {
|
||||
param(
|
||||
$GitProfile = $null
|
||||
)
|
||||
if ($null -eq $GitProfile) {
|
||||
$GitProfile = Get-GitSkillsProfile
|
||||
}
|
||||
if ($null -eq $GitProfile) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$lines = New-Object System.Collections.Generic.List[string]
|
||||
$primaryName = ([string]$GitProfile.userName).Trim()
|
||||
$primaryEmail = ([string]$GitProfile.userEmail).Trim()
|
||||
if (-not [string]::IsNullOrWhiteSpace($primaryName) -and -not [string]::IsNullOrWhiteSpace($primaryEmail)) {
|
||||
[void]$lines.Add(('Co-authored-by: {0} <{1}>' -f $primaryName, $primaryEmail))
|
||||
}
|
||||
|
||||
$extras = @()
|
||||
if ($GitProfile.PSObject.Properties.Name -contains 'coAuthors' -and $null -ne $GitProfile.coAuthors) {
|
||||
$extras = @($GitProfile.coAuthors)
|
||||
}
|
||||
foreach ($item in $extras) {
|
||||
$name = ''
|
||||
$email = ''
|
||||
if ($null -eq $item) { continue }
|
||||
if ($item -is [string]) {
|
||||
if ($item -match '^\s*(.+?)\s*<([^>]+)>\s*$') {
|
||||
$name = $Matches[1].Trim()
|
||||
$email = $Matches[2].Trim()
|
||||
}
|
||||
}
|
||||
else {
|
||||
$name = ([string]$item.name).Trim()
|
||||
$email = ([string]$item.email).Trim()
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($name) -or [string]::IsNullOrWhiteSpace($email)) {
|
||||
continue
|
||||
}
|
||||
$line = 'Co-authored-by: {0} <{1}>' -f $name, $email
|
||||
if (-not $lines.Contains($line)) {
|
||||
[void]$lines.Add($line)
|
||||
}
|
||||
}
|
||||
|
||||
return @($lines)
|
||||
}
|
||||
|
||||
function Format-CommitMessageWithTrailers {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Subject,
|
||||
$GitProfile = $null
|
||||
)
|
||||
$subject = $Subject.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($subject)) {
|
||||
throw 'Commit subject is empty.'
|
||||
}
|
||||
$trailers = Get-CoAuthorTrailerLines -GitProfile $GitProfile
|
||||
if ($trailers.Count -eq 0) {
|
||||
return $subject
|
||||
}
|
||||
return ($subject + "`n`n" + ($trailers -join "`n"))
|
||||
}
|
||||
Reference in New Issue
Block a user