2cddf06b6c
Co-authored-by: 旅行呀~ <travelxiao@qq.com>
292 lines
8.7 KiB
PowerShell
292 lines
8.7 KiB
PowerShell
# 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"))
|
|
}
|