Public/推送-GitHub.ps1
|
function 推送-分支 { # 推送分支;若被远端以 non-fast-forward 拒绝,交互询问是否用 # --force-with-lease 强推覆盖远端。确认通过则改为强推并重试。 param( [Parameter(Mandatory = $true)] [string[]]$推送参数, [Parameter()] [AllowEmptyString()] [string]$代理 = "", [Parameter()] [string]$远程名 = "", [Parameter()] [string]$分支 = "" ) while ($true) { # 不捕获 git 输出:让 stderr(进度与错误信息均走 stderr)直接流向控制台实时显示,避免重定向进管道后被行缓冲卡住。 # 失败原因事后用 fetch + 祖先关系判断,不依赖解析错误文本。 $原错误策略 = $ErrorActionPreference $ErrorActionPreference = "Continue" & git @(组成-Git参数 $推送参数 $代理) $退出码 = $LASTEXITCODE $ErrorActionPreference = $原错误策略 if ($退出码 -eq 0) { return } $已强推过 = $推送参数 -contains "--force-with-lease" if ($已强推过) { throw "强推仍被远端拒绝(远端在你 fetch 后又有新提交),已停止以防误覆盖,请拉取后重试。" } # fetch 远端最新状态,判断失败是否为分支分叉(non-fast-forward) $获取成功 = 尝试-Git命令 @("fetch", $远程名) -代理 $代理 if (-not $获取成功) { throw "git push 执行失败,且无法获取远端状态,请检查网络或代理(错误详情见上方 git 输出)。" } $远端提交 = 尝试读取-Git文本 @("rev-parse", "$远程名/$分支") if (-not [string]::IsNullOrWhiteSpace($远端提交)) { # 远端分支尖端不是本地 HEAD 的祖先 => 分叉(远端有本地没有的提交) $远端在本地历史内 = 尝试-Git命令 @("merge-base", "--is-ancestor", $远端提交, "HEAD") if (-not $远端在本地历史内) { Write-Host "" Write-Host "远端 $远程名/$分支 上存在本地没有的提交,普通推送无法完成。" -ForegroundColor Yellow $选择 = $null while ($null -eq $选择 -or $选择 -notmatch "^[yYnN]$") { $输入 = Read-Host "是否强推覆盖远端?(--force-with-lease;远端独有的提交将丢失,请谨慎)[y/N]" if ([string]::IsNullOrWhiteSpace($输入)) { $选择 = "n" } else { $选择 = $输入.Substring(0, 1) } } if ($选择 -match "^[yY]$") { Write-Host "将以 --force-with-lease 强推覆盖远端(刚才已 fetch,校验基准为最新)..." -ForegroundColor Yellow $推送参数 = @($推送参数[0], "--force-with-lease") + @($推送参数 | Select-Object -Skip 1) continue } throw "已放弃推送。远端领先于本地,请先拉取并合并远端的更改后再推送。" } } # 到这里说明不是分叉:可能是 pre-push 钩子失败(如 Git LFS 懒加载超时 / missing object)、权限不足等,真实原因已在上方 git 输出中打印。 throw "git push 执行失败,具体原因见上方 git 输出(常见原因:pre-push 钩子失败(如 LFS 对象懒加载失败)、权限不足等)。" } } function LFS推送范围有改动 { # 检测本次推送范围(HEAD 相对所有远程的新提交)内是否改动了 LFS指针文件。合并提交不参与检测(git log --name-only 对合并提交默认不输出文件名)。任何检测失败都保守地返回 $true(宁可多传 LFS,也不误跳过)。 try { $原错误策略 = $ErrorActionPreference $ErrorActionPreference = "Continue" $路径行 = @(git log --name-only --pretty=format: HEAD --not --remotes 2>$null) $log退出码 = $LASTEXITCODE $ErrorActionPreference = $原错误策略 if ($log退出码 -ne 0) { return $true } $改动路径 = @($路径行 | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() } | Select-Object -Unique) if ($改动路径.Count -eq 0) { return $false } # 分批以命令行参数方式查询 filter 属性(不走 stdin,避免编码问题),值为 lfs 即 LFS 指针文件被改动 for ($批 = 0; $批 -lt [math]::Ceiling($改动路径.Count / 100); $批++) { $段 = @($改动路径 | Select-Object -Skip ($批 * 100) -First 100) $属性行 = @(& git @(@("check-attr", "filter", "--") + $段) 2>$null) if ($LASTEXITCODE -ne 0) { return $true } foreach ($行 in $属性行) { if ($行 -match ': filter: lfs$') { return $true } } } return $false } catch { return $true } } function 是-LFS仓库 { if (Test-Path ".git\lfs") { return $true } if (Test-Path ".git\hooks\pre-push") { return [bool](Select-String -LiteralPath ".git\hooks\pre-push" -Pattern "git[l]fs" -Quiet) } return $false } function 确保-Fetch引用规范 { # 某些 fork(如 GitHub CLI / API 创建的)的远程可能缺少 fetch refspec,导致 fetch 不生成 refs/remotes/<远程>/* 引用,分支跟踪名存实亡(git push -u 只写 branch.<分支>.merge 配置,但不产生远端跟踪引用)。检测到缺失时自动补上标准 refspec,并立即 fetch 当前分支生成跟踪引用。 param( [Parameter(Mandatory = $true)] [string]$远程名, [Parameter(Mandatory = $true)] [string]$分支, [Parameter()] [AllowEmptyString()] [string]$代理 = "" ) $原错误策略 = $ErrorActionPreference $ErrorActionPreference = "Continue" $现有规范 = @(git config --get-all "remote.$远程名.fetch" 2>$null) $ErrorActionPreference = $原错误策略 if ($现有规范.Count -gt 0) { return } Write-Host "检测到远程 $远程名 缺少 fetch refspec(此类仓库的分支跟踪会失效),自动补上标准配置..." -ForegroundColor Cyan git config "remote.$远程名.fetch" "+refs/heads/*:refs/remotes/$远程名/*" # 立即获取当前分支,生成 refs/remotes/<远程>/<分支>,使跟踪(@{u})真正可用 $原错误策略 = $ErrorActionPreference $ErrorActionPreference = "Continue" & git @(组成-Git参数 @("fetch", "--progress", $远程名, "${分支}:refs/remotes/$远程名/$分支") $代理) 2>&1 | ForEach-Object { Write-Host $_ } $ErrorActionPreference = $原错误策略 } <# .SYNOPSIS 将本地提交推送到 GitHub 仓库,支持可选代理。 .DESCRIPTION 在指定仓库目录(默认当前目录)中向远程推送当前分支。 若当前分支尚未建立跟踪(上游)关系,推送时会自动建立跟踪(git push -u), 以后即可直接用 git pull/push 等命令。 不指定 -远程名 时自动解析:优先使用 origin;没有 origin 且仅有一个远程则使用该远程; 没有 origin 且存在多个远程时报错,需显式指定 -远程名。 代理只需通过 -代理 参数指定一次,之后永久记住,以后直接运行 推送-GitHub 会自动使用记住的代理。代理仅以命令行参数 -c http.proxy / -c https.proxy 的形式在本次进程内生效,不写入任何 git 配置文件、不修改全局环境变量,因此完全不影响用户手动执行 git push 或其他 git 命令。指定空字符串("")可清除已记住的代理,恢复直连推送。 若推送被远端以 non-fast-forward 拒绝(远端存在本地没有的提交),会交互式询问是否强推覆盖远端:确认则使用 --force-with-lease 强推,否则放弃推送。 若仓库启用 Git LFS,会自动检测本次推送范围的提交是否真正改动了 LFS 文件:未改动则自动跳过 LFS 对象上传(仅推送 git 对象与 LFS 指针,等同于 GIT_LFS_SKIP_PUSH=1),避免为上游的大体积 LFS 数据白等;改动了则正常上传。 若检测到远程缺少 fetch refspec(常见于 GitHub CLI/API 创建的 fork,会导致分支跟踪名存实亡),会自动补上标准 refspec 并 fetch 生成跟踪引用。 .PARAMETER 仓库目录 第一个位置参数。仓库所在目录路径,默认为当前目录。 .PARAMETER 代理 可选。代理地址,如 "http://127.0.0.1:7890" 或 "socks5://127.0.0.1:1080"。 指定后自动保存,无需再次指定;以后每次推送自动使用记住的代理。 传入空字符串可清除已记住的代理。不指定则沿用已记住的代理(若无则直连)。 .PARAMETER 远程名 可选。Git 远程名称。不指定时自动选择:有 origin 则用 origin; 无 origin 且只有一个远程则用该远程;无 origin 且有多个远程则报错。 .PARAMETER 记录文件路径 镜像统计记录文件的保存路径(代理也保存在此文件中),默认为 "$env:TEMP\镜像拉取记录.json"。 .EXAMPLE 推送-GitHub -代理 "http://127.0.0.1:7890" 首次推送:通过指定代理推送,并永久记住该代理。 .EXAMPLE 推送-GitHub 以后推送:自动使用记住的代理和自动解析的远程。 .EXAMPLE 推送-GitHub "D:\Repo" 推送指定目录下的仓库(仓库目录为第一个位置参数,可不写参数名)。 .EXAMPLE 推送-GitHub -代理 "" 清除记住的代理并直连推送。 #> function 推送-GitHub { [CmdletBinding()] param( [Parameter(Position = 0)] [string]$仓库目录 = ".", [Parameter()] [AllowEmptyString()] [string]$代理, [Parameter()] [string]$远程名, [Parameter()] [string]$记录文件路径 = $script:默认记录路径 ) if (-not (Test-Path -LiteralPath $仓库目录 -PathType Container)) { throw "仓库目录不存在或不是目录:$仓库目录" } $仓库目录 = (Resolve-Path -LiteralPath $仓库目录).Path # 相对记录文件路径先按调用者当前目录解析,避免进入仓库目录后基准改变 if (-not [System.IO.Path]::IsPathRooted($记录文件路径)) { $记录文件路径 = Join-Path (Get-Location).Path $记录文件路径 } Push-Location $仓库目录 try { $分支 = 读取-Git文本 @("branch", "--show-current") if ([string]::IsNullOrWhiteSpace($分支)) { throw "当前处于分离 HEAD 状态,无法执行推送。" } if ([string]::IsNullOrWhiteSpace($远程名)) { $远程名 = 解析-推送远程名 Write-Host "自动选择远程:$远程名" } # 显式加 --progress:输出被重定向时仍可看到实时推送进度 $记录 = 读取-镜像记录 $记录文件路径 if ($PSBoundParameters.ContainsKey("代理")) { # 用户显式指定了代理:保存并永久记住;显式传空字符串则清除代理 $记录.代理 = $代理 保存-镜像记录 $记录 $记录文件路径 Write-Host "已保存推送代理,以后无需再指定:$代理" } $使用代理 = 获取-已存代理 $记录 if ([string]::IsNullOrWhiteSpace($使用代理)) { Write-Host "未设置代理,直连推送。" } else { Write-Host "将通过已记住的代理推送:$使用代理" } $自动跳过LFS = $false if ((是-LFS仓库) -and -not (LFS推送范围有改动)) { $env:GIT_LFS_SKIP_PUSH = "1" $自动跳过LFS = $true Write-Host "检测到推送范围内的提交未改动 LFS 文件,自动跳过 LFS 对象上传。" -ForegroundColor Cyan } # 确保远程有 fetch refspec(fork 可能缺失),否则分支跟踪名存实亡 确保-Fetch引用规范 $远程名 $分支 -代理 $使用代理 # 检查当前分支是否已有跟踪(上游) $上游 = 尝试读取-Git文本 @("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}") if ([string]::IsNullOrWhiteSpace($上游)) { # 尚未设置跟踪,推送时用 -u 一并建立 Write-Host "当前分支尚无跟踪,将推送并建立跟踪关系。" 推送-分支 @("push", "--progress", "-u", $远程名, $分支) -代理 $使用代理 -远程名 $远程名 -分支 $分支 } else { 推送-分支 @("push", "--progress", $远程名, $分支) -代理 $使用代理 -远程名 $远程名 -分支 $分支 } Write-Host "推送完成。已推送 $分支 到 $远程名。" } finally { if ($自动跳过LFS) { Remove-Item Env:GIT_LFS_SKIP_PUSH -ErrorAction SilentlyContinue } Pop-Location } } |