@caddis/cli 0.4.19 → 0.4.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/skills/_registry.md +4 -2
- package/bundles/antigravity-plugin/skills/data-lineage/SKILL.md +32 -0
- package/bundles/antigravity-plugin/skills/hld/SKILL.md +20 -8
- package/bundles/antigravity-plugin/skills/lld/SKILL.md +27 -12
- package/bundles/antigravity-plugin/skills/port-to-github/SKILL.md +231 -0
- package/bundles/antigravity-plugin/skills/port-to-github/references/ci-and-credentials.md +66 -0
- package/bundles/antigravity-plugin/skills/port-to-github/references/config.example.json +18 -0
- package/bundles/antigravity-plugin/skills/port-to-github/references/old-host.md +61 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Compare-PortRefs.ps1 +68 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Convert-PortWorkflow.ps1 +92 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Get-PortSource.ps1 +459 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/PortCommon.ps1 +130 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Publish-PortToGitHub.ps1 +156 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Switch-CheckoutOrigin.ps1 +182 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Test-PortReadiness.ps1 +218 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Test-PortWorkflowParses.ps1 +62 -0
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +3 -2
- package/bundles/antigravity-plugin-extras/skills/data-lineage/SKILL.md +158 -0
- package/bundles/antigravity-plugin-extras/skills/data-lineage/scripts/convert_doc_to_confluence.py +1430 -0
- package/bundles/antigravity-plugin-extras/skills/hld/SKILL.md +155 -35
- package/bundles/antigravity-plugin-extras/skills/hld/references/gold-standard-hld.md +419 -0
- package/bundles/antigravity-plugin-extras/skills/hld/scripts/convert_doc_to_confluence.py +900 -184
- package/bundles/antigravity-plugin-extras/skills/lld/SKILL.md +145 -41
- package/bundles/antigravity-plugin-extras/skills/lld/scripts/convert_doc_to_confluence.py +900 -184
- package/bundles/codex/.codex/skills/devops/port-to-github/SKILL.md +231 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/references/ci-and-credentials.md +66 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/references/config.example.json +18 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/references/old-host.md +61 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Compare-PortRefs.ps1 +68 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Convert-PortWorkflow.ps1 +92 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Get-PortSource.ps1 +459 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/PortCommon.ps1 +130 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Publish-PortToGitHub.ps1 +156 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Switch-CheckoutOrigin.ps1 +182 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Test-PortReadiness.ps1 +218 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Test-PortWorkflowParses.ps1 +62 -0
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Copy an app from ANY source into a local staging bare repository, ready to push to GitHub.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Source kinds, detected from -Source:
|
|
7
|
+
url https://, http://, ssh://, git://, or user@host:path (Gitea, GitLab, Bitbucket, any git server)
|
|
8
|
+
local a folder on this machine, with or without a .git
|
|
9
|
+
unc \\server\share\folder, with or without a .git
|
|
10
|
+
winrm winrm://HOST/C:/path, or -ComputerName HOST with -Source C:\path
|
|
11
|
+
|
|
12
|
+
Every kind ends in the same place: <staging>\<Name>\mirror.git, a bare repository holding
|
|
13
|
+
only refs/heads/* and refs/tags/*, with NO remote configured (so a stray push can never go
|
|
14
|
+
back to the source), plus <staging>\<Name>\manifest.json describing what was found.
|
|
15
|
+
|
|
16
|
+
The source is never modified. A folder without git history becomes one import commit.
|
|
17
|
+
|
|
18
|
+
.EXAMPLE
|
|
19
|
+
./Get-PortSource.ps1 -Source http://gitea.example:3000/org/app.git
|
|
20
|
+
./Get-PortSource.ps1 -Source D:\work\app -IncludeUncommitted
|
|
21
|
+
./Get-PortSource.ps1 -Source \\fileserver\projects\app
|
|
22
|
+
./Get-PortSource.ps1 -Source winrm://prodbox/D:/apps/app
|
|
23
|
+
#>
|
|
24
|
+
[CmdletBinding()]
|
|
25
|
+
param(
|
|
26
|
+
[Parameter(Mandatory)][string]$Source,
|
|
27
|
+
[string]$ComputerName,
|
|
28
|
+
[pscredential]$Credential,
|
|
29
|
+
[string]$Name,
|
|
30
|
+
[string]$StagingRoot,
|
|
31
|
+
[string]$ConfigPath,
|
|
32
|
+
# Put uncommitted and untracked changes on a separate branch instead of leaving them behind.
|
|
33
|
+
[switch]$IncludeUncommitted,
|
|
34
|
+
# Turn remote-tracking branches of this remote (e.g. 'origin') into real branches when the
|
|
35
|
+
# source has no local branch of that name. Use when a checkout is the only surviving copy.
|
|
36
|
+
[string]$PromoteRemoteBranches,
|
|
37
|
+
# What to do when a plain folder or an uncommitted snapshot contains a likely secret.
|
|
38
|
+
[ValidateSet('Stop', 'Exclude')][string]$OnSecret = 'Stop',
|
|
39
|
+
[string]$InitialBranch = 'main',
|
|
40
|
+
# Replace an existing staging folder for this Name.
|
|
41
|
+
[switch]$Force
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
$ErrorActionPreference = 'Stop'
|
|
45
|
+
. (Join-Path $PSScriptRoot 'PortCommon.ps1')
|
|
46
|
+
|
|
47
|
+
$config = Get-PortConfig $ConfigPath
|
|
48
|
+
$stagingBase = Get-PortStagingRoot $config $StagingRoot
|
|
49
|
+
|
|
50
|
+
# ---- 1. Work out what kind of source this is -------------------------------------------
|
|
51
|
+
$remotePath = $null
|
|
52
|
+
if ($Source -match '^winrm://(?<host>[^/]+)/(?<path>.+)$') {
|
|
53
|
+
$ComputerName = $Matches.host
|
|
54
|
+
$remotePath = ($Matches.path -replace '/', '\')
|
|
55
|
+
}
|
|
56
|
+
elseif ($ComputerName) {
|
|
57
|
+
$remotePath = $Source
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if ($ComputerName) { $kind = 'winrm' }
|
|
61
|
+
elseif ($Source -match '^(https?|ssh|git)://' -or $Source -match '^[^@\s\\/]+@[^:\s]+:') { $kind = 'url' }
|
|
62
|
+
elseif ($Source -match '^\\\\') { $kind = 'unc' }
|
|
63
|
+
else { $kind = 'local' }
|
|
64
|
+
|
|
65
|
+
if (-not $Name) {
|
|
66
|
+
$leaf = ($Source.TrimEnd('/', '\') -split '[\\/:]')[-1]
|
|
67
|
+
$Name = $leaf -replace '\.git$', ''
|
|
68
|
+
}
|
|
69
|
+
if ($Name -notmatch '^[A-Za-z0-9._-]+$') { throw "Name '$Name' is not a valid GitHub repository name. Pass -Name." }
|
|
70
|
+
|
|
71
|
+
$stage = Join-Path $stagingBase $Name
|
|
72
|
+
if (Test-Path -LiteralPath $stage) {
|
|
73
|
+
if (-not $Force) { throw "Staging folder $stage already exists. Pass -Force to replace it." }
|
|
74
|
+
Remove-Item -LiteralPath $stage -Recurse -Force
|
|
75
|
+
}
|
|
76
|
+
New-Item -ItemType Directory -Force -Path $stage | Out-Null
|
|
77
|
+
$mirror = Join-Path $stage 'mirror.git'
|
|
78
|
+
$work = Join-Path $stage 'work'
|
|
79
|
+
|
|
80
|
+
$manifest = [ordered]@{
|
|
81
|
+
name = $Name
|
|
82
|
+
sourceKind = $kind
|
|
83
|
+
source = (Remove-UrlCredential $Source).Url
|
|
84
|
+
computerName = $ComputerName
|
|
85
|
+
capturedAt = (Get-Date).ToString('o')
|
|
86
|
+
hadGitHistory = $false
|
|
87
|
+
sourceHead = $null
|
|
88
|
+
defaultBranch = $null
|
|
89
|
+
dirtyFiles = @()
|
|
90
|
+
snapshotBranch = $null
|
|
91
|
+
remoteOnlyBranches = @()
|
|
92
|
+
promotedBranches = @()
|
|
93
|
+
droppedRefs = @()
|
|
94
|
+
secretFindings = @()
|
|
95
|
+
excludedSecretFiles = @()
|
|
96
|
+
suspectDirs = @()
|
|
97
|
+
largeFiles = @()
|
|
98
|
+
heads = @()
|
|
99
|
+
tags = @()
|
|
100
|
+
status = 'in-progress'
|
|
101
|
+
warnings = @()
|
|
102
|
+
}
|
|
103
|
+
if ((Remove-UrlCredential $Source).HadCredential) {
|
|
104
|
+
$manifest.warnings += 'The source URL embedded a credential. It was used for the clone and is not recorded. Consider rotating it.'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function Save-Manifest { ConvertTo-PortJsonFile $manifest (Join-Path $stage 'manifest.json') }
|
|
108
|
+
|
|
109
|
+
# ---- helpers used by more than one kind ---------------------------------------------------
|
|
110
|
+
|
|
111
|
+
function Find-Secrets([string]$Root, [string[]]$RelativeFiles) {
|
|
112
|
+
$found = @()
|
|
113
|
+
foreach ($rel in $RelativeFiles) {
|
|
114
|
+
$full = Join-Path $Root $rel
|
|
115
|
+
if (-not (Test-Path -LiteralPath $full -PathType Leaf)) { continue }
|
|
116
|
+
$leafName = Split-Path $rel -Leaf
|
|
117
|
+
if (Test-PortSecretName $leafName) {
|
|
118
|
+
$found += [pscustomobject]@{ file = ($rel -replace '\\', '/'); reason = 'file name' }
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
$info = Get-Item -LiteralPath $full
|
|
122
|
+
if ($info.Length -gt 1MB) { continue }
|
|
123
|
+
$text = $null
|
|
124
|
+
try { $text = [IO.File]::ReadAllText($full) } catch { continue }
|
|
125
|
+
if ($text -match "`0") { continue } # binary
|
|
126
|
+
$hit = Test-PortSecretContent $text
|
|
127
|
+
if ($hit) { $found += [pscustomobject]@{ file = ($rel -replace '\\', '/'); reason = "content matches $hit" } }
|
|
128
|
+
}
|
|
129
|
+
return , $found
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function Resolve-Secrets([string]$Root, [string[]]$RelativeFiles, [string]$Context) {
|
|
133
|
+
$found = Find-Secrets $Root $RelativeFiles
|
|
134
|
+
if ($found.Count -eq 0) { return @() }
|
|
135
|
+
$manifest.secretFindings += $found
|
|
136
|
+
foreach ($f in $found) { Write-PortWarn "Likely secret in ${Context}: $($f.file) ($($f.reason))" }
|
|
137
|
+
if ($OnSecret -eq 'Stop') {
|
|
138
|
+
$manifest.status = 'blocked-secrets'
|
|
139
|
+
Save-Manifest
|
|
140
|
+
Write-PortFail "Stopped: $($found.Count) likely secret file(s). Nothing was pushed anywhere. Review them, then re-run with -OnSecret Exclude to leave them out, or move the secret out of the file."
|
|
141
|
+
exit 2
|
|
142
|
+
}
|
|
143
|
+
foreach ($f in $found) {
|
|
144
|
+
Remove-Item -LiteralPath (Join-Path $Root $f.file) -Force
|
|
145
|
+
$manifest.excludedSecretFiles += $f.file
|
|
146
|
+
}
|
|
147
|
+
return @($found | ForEach-Object { $_.file })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function Copy-Tree([string]$From, [string]$To) {
|
|
151
|
+
New-Item -ItemType Directory -Force -Path $To | Out-Null
|
|
152
|
+
$xd = @($script:PortJunkDirs)
|
|
153
|
+
& robocopy $From $To /E /R:1 /W:1 /NFL /NDL /NJH /NJS /NP /XJ /XD @xd | Out-Null
|
|
154
|
+
if ($LASTEXITCODE -ge 8) { throw "robocopy $From -> $To failed with exit $LASTEXITCODE" }
|
|
155
|
+
$global:LASTEXITCODE = 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function Import-PlainTree([string]$Tree) {
|
|
159
|
+
# A folder with no history becomes ONE import commit on $InitialBranch.
|
|
160
|
+
$files = @(Get-ChildItem -LiteralPath $Tree -Recurse -File -Force | ForEach-Object { $_.FullName.Substring($Tree.Length).TrimStart('\', '/') })
|
|
161
|
+
if ($files.Count -eq 0) { throw "The source folder has no files to import." }
|
|
162
|
+
$excluded = @(Resolve-Secrets $Tree $files 'the source folder')
|
|
163
|
+
|
|
164
|
+
foreach ($d in $script:PortSuspectDirs) {
|
|
165
|
+
if (Test-Path -LiteralPath (Join-Path $Tree $d) -PathType Container) { $manifest.suspectDirs += $d }
|
|
166
|
+
}
|
|
167
|
+
foreach ($f in (Get-ChildItem -LiteralPath $Tree -Recurse -File -Force | Where-Object { $_.Length -gt 50MB })) {
|
|
168
|
+
$manifest.largeFiles += [pscustomobject]@{ file = $f.FullName.Substring($Tree.Length).TrimStart('\'); bytes = $f.Length }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
$gi = Join-Path $Tree '.gitignore'
|
|
172
|
+
$ignoreLines = @()
|
|
173
|
+
if (-not (Test-Path -LiteralPath $gi)) {
|
|
174
|
+
$ignoreLines += '# Written by port-to-github for a folder that had no .gitignore.'
|
|
175
|
+
$ignoreLines += ($script:PortJunkDirs | Where-Object { $_ -ne '.git' } | ForEach-Object { "$_/" })
|
|
176
|
+
$ignoreLines += @('.env', '.env.*', '!.env.example', '*.log')
|
|
177
|
+
}
|
|
178
|
+
if ($excluded.Count -gt 0) {
|
|
179
|
+
$ignoreLines += '# Likely secrets left out of the import by port-to-github:'
|
|
180
|
+
$ignoreLines += ($excluded | ForEach-Object { "/$_" })
|
|
181
|
+
}
|
|
182
|
+
if ($ignoreLines.Count -gt 0) { Add-Content -LiteralPath $gi -Value $ignoreLines -Encoding UTF8 }
|
|
183
|
+
|
|
184
|
+
Invoke-PortGit @('init', '-q', '-b', $InitialBranch, $Tree) | Out-Null
|
|
185
|
+
$email = Invoke-PortGit -AllowFailure @('-C', $Tree, 'config', 'user.email')
|
|
186
|
+
if ($email.ExitCode -ne 0 -or -not ($email.Output -join '')) {
|
|
187
|
+
throw "git has no user.email for this folder. Set your identity (git config --global user.email ...) and re-run."
|
|
188
|
+
}
|
|
189
|
+
# A brand-new repository built from a copied folder: adding everything IS the import.
|
|
190
|
+
Invoke-PortGit @('-C', $Tree, 'add', '-A') | Out-Null
|
|
191
|
+
Invoke-PortGit @('-C', $Tree, 'commit', '-q', '-m', "Import $Name from a $kind folder (no prior git history)", '-m', "Source: $($manifest.source)") | Out-Null
|
|
192
|
+
Invoke-PortGit @('clone', '-q', '--mirror', $Tree, $mirror) | Out-Null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function Add-Snapshot([string]$DirtyRoot, [string[]]$Changed, [string[]]$Deleted, [string]$BaseRef) {
|
|
196
|
+
# Uncommitted work goes on its OWN branch, so the real branches stay exactly as committed.
|
|
197
|
+
$snapTree = Join-Path $work 'snapshot'
|
|
198
|
+
Invoke-PortGit @('clone', '-q', $mirror, $snapTree) | Out-Null
|
|
199
|
+
Invoke-PortGit @('-C', $snapTree, 'checkout', '-q', '--detach', $BaseRef) | Out-Null
|
|
200
|
+
$branch = 'port/uncommitted-snapshot-' + (Get-Date -Format 'yyyyMMdd-HHmm')
|
|
201
|
+
Invoke-PortGit @('-C', $snapTree, 'checkout', '-q', '-b', $branch) | Out-Null
|
|
202
|
+
foreach ($rel in $Changed) {
|
|
203
|
+
$src = Join-Path $DirtyRoot $rel
|
|
204
|
+
if (-not (Test-Path -LiteralPath $src -PathType Leaf)) { continue }
|
|
205
|
+
$dst = Join-Path $snapTree $rel
|
|
206
|
+
New-Item -ItemType Directory -Force -Path (Split-Path $dst -Parent) | Out-Null
|
|
207
|
+
Copy-Item -LiteralPath $src -Destination $dst -Force
|
|
208
|
+
}
|
|
209
|
+
foreach ($rel in $Deleted) {
|
|
210
|
+
$dst = Join-Path $snapTree $rel
|
|
211
|
+
if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Force }
|
|
212
|
+
}
|
|
213
|
+
$excluded = @(Resolve-Secrets $snapTree $Changed 'the uncommitted changes')
|
|
214
|
+
foreach ($rel in $excluded) { Invoke-PortGit -AllowFailure @('-C', $snapTree, 'rm', '-q', '--cached', '--ignore-unmatch', '--', $rel) | Out-Null }
|
|
215
|
+
# A throwaway clone that holds only the snapshot: adding everything is the snapshot.
|
|
216
|
+
Invoke-PortGit @('-C', $snapTree, 'add', '-A') | Out-Null
|
|
217
|
+
$pending = Invoke-PortGit @('-C', $snapTree, 'status', '--porcelain')
|
|
218
|
+
if (-not ($pending -join '')) { Write-PortWarn 'The uncommitted changes produced no difference. No snapshot branch.'; return }
|
|
219
|
+
Invoke-PortGit @('-C', $snapTree, 'commit', '-q', '-m', "Snapshot of uncommitted work found in the source during the port", '-m', "Source: $($manifest.source). Base: $BaseRef.") | Out-Null
|
|
220
|
+
Invoke-PortGit @('-C', $snapTree, 'push', '-q', $mirror, "HEAD:refs/heads/$branch") | Out-Null
|
|
221
|
+
$manifest.snapshotBranch = $branch
|
|
222
|
+
Write-PortOk "Uncommitted work saved on branch $branch"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# ---- 2. Acquire -----------------------------------------------------------------------------
|
|
226
|
+
Write-PortStep "Source kind: $kind"
|
|
227
|
+
switch ($kind) {
|
|
228
|
+
'url' {
|
|
229
|
+
Write-PortStep "Mirror-cloning $($manifest.source)"
|
|
230
|
+
Invoke-PortGit @('clone', '-q', '--mirror', $Source, $mirror) | Out-Null
|
|
231
|
+
$manifest.hadGitHistory = $true
|
|
232
|
+
}
|
|
233
|
+
{ $_ -in 'local', 'unc' } {
|
|
234
|
+
if (-not (Test-Path -LiteralPath $Source -PathType Container)) { throw "Folder not found: $Source" }
|
|
235
|
+
$full = (Resolve-Path -LiteralPath $Source).ProviderPath.TrimEnd('\')
|
|
236
|
+
$top = Invoke-PortGit -AllowFailure @('-C', $full, 'rev-parse', '--show-toplevel')
|
|
237
|
+
$bare = Invoke-PortGit -AllowFailure @('-C', $full, 'rev-parse', '--is-bare-repository')
|
|
238
|
+
$isBare = ($bare.ExitCode -eq 0 -and ($bare.Output -join '') -eq 'true')
|
|
239
|
+
$isRepo = $isBare
|
|
240
|
+
if (-not $isBare -and $top.ExitCode -eq 0) {
|
|
241
|
+
$topPath = ($top.Output -join '').Replace('/', '\').TrimEnd('\')
|
|
242
|
+
if ($topPath -ieq $full) { $isRepo = $true }
|
|
243
|
+
else {
|
|
244
|
+
throw "$full is a SUBFOLDER of the repository $topPath. Port the whole repository, or split the folder out first (git subtree split --prefix=<dir>) and port the result."
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if ($isRepo) {
|
|
248
|
+
Write-PortStep "Folder is a git repository. Mirror-cloning it (history is kept)."
|
|
249
|
+
Invoke-PortGit @('clone', '-q', '--mirror', '--no-local', $full, $mirror) | Out-Null
|
|
250
|
+
$manifest.hadGitHistory = $true
|
|
251
|
+
if (-not $isBare) {
|
|
252
|
+
$headRes = Invoke-PortGit -AllowFailure @('-C', $full, 'symbolic-ref', '--short', 'HEAD')
|
|
253
|
+
$manifest.sourceHead = if ($headRes.ExitCode -eq 0) { $headRes.Output -join '' } else { (Invoke-PortGit @('-C', $full, 'rev-parse', 'HEAD')) -join '' }
|
|
254
|
+
$manifest.dirtyFiles = @(Invoke-PortGit @('-C', $full, 'status', '--porcelain', '--untracked-files=all'))
|
|
255
|
+
if ($manifest.dirtyFiles.Count -gt 0) {
|
|
256
|
+
if ($IncludeUncommitted) {
|
|
257
|
+
$changed = @(Invoke-PortGit @('-C', $full, 'ls-files', '-m', '-o', '--exclude-standard'))
|
|
258
|
+
$deleted = @(Invoke-PortGit @('-C', $full, 'ls-files', '-d'))
|
|
259
|
+
$baseRef = (Invoke-PortGit @('-C', $full, 'rev-parse', 'HEAD')) -join ''
|
|
260
|
+
Add-Snapshot $full $changed $deleted $baseRef
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
$manifest.warnings += "The source has $($manifest.dirtyFiles.Count) uncommitted change(s). They are NOT included. Re-run with -IncludeUncommitted to keep them on a separate branch."
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
Write-PortStep "Folder has no git history. Importing it as one commit."
|
|
270
|
+
$tree = Join-Path $work 'tree'
|
|
271
|
+
Copy-Tree $full $tree
|
|
272
|
+
Import-PlainTree $tree
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
'winrm' {
|
|
276
|
+
Write-PortStep "Reading $remotePath on $ComputerName over WinRM"
|
|
277
|
+
$sessArgs = @{ ComputerName = $ComputerName }
|
|
278
|
+
if ($Credential) { $sessArgs.Credential = $Credential }
|
|
279
|
+
$session = New-PSSession @sessArgs
|
|
280
|
+
$remote = $null
|
|
281
|
+
try {
|
|
282
|
+
# Runs on the remote box under Windows PowerShell 5.1: keep it 5.1-compatible.
|
|
283
|
+
$remote = Invoke-Command -Session $session -ArgumentList $remotePath, [bool]$IncludeUncommitted, $script:PortJunkDirs -ScriptBlock {
|
|
284
|
+
param($Path, $WantDirty, $JunkDirs)
|
|
285
|
+
# 'Continue', not 'Stop': under Windows PowerShell 5.1 a native command's stderr
|
|
286
|
+
# becomes a TERMINATING error with 'Stop', even with 2>$null - so probing a plain
|
|
287
|
+
# folder with git would kill the block. Git is judged by $LASTEXITCODE instead;
|
|
288
|
+
# cmdlets that must not fail carry -ErrorAction Stop.
|
|
289
|
+
$ErrorActionPreference = 'Continue'
|
|
290
|
+
$r = @{ exists = $false; isRepo = $false; tmp = $null; bundle = $null; treeZip = $null; dirtyZip = $null; dirty = @(); changed = @(); deleted = @(); head = $null; headSha = $null; error = $null }
|
|
291
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Container)) { return $r }
|
|
292
|
+
try {
|
|
293
|
+
$r.exists = $true
|
|
294
|
+
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
295
|
+
$tmp = Join-Path $env:TEMP ('port-to-github-' + [guid]::NewGuid().ToString('N'))
|
|
296
|
+
New-Item -ItemType Directory -Path $tmp -ErrorAction Stop | Out-Null
|
|
297
|
+
$r.tmp = $tmp
|
|
298
|
+
$git = $null
|
|
299
|
+
$cmd = Get-Command git -ErrorAction SilentlyContinue
|
|
300
|
+
if ($cmd) { $git = $cmd.Source } elseif (Test-Path 'C:\Program Files\Git\cmd\git.exe') { $git = 'C:\Program Files\Git\cmd\git.exe' }
|
|
301
|
+
$isRepo = $false
|
|
302
|
+
$isBare = $false
|
|
303
|
+
if ($git) {
|
|
304
|
+
# A BARE repository has no work tree, so --show-toplevel fails on it. Check
|
|
305
|
+
# for bare first, or the whole history is imported as one plain-folder commit.
|
|
306
|
+
$bareOut = & $git -c safe.directory=* -C $Path rev-parse --is-bare-repository 2>$null
|
|
307
|
+
if ($LASTEXITCODE -eq 0 -and "$bareOut" -eq 'true') { $isRepo = $true; $isBare = $true }
|
|
308
|
+
}
|
|
309
|
+
if ($git -and -not $isBare) {
|
|
310
|
+
$top = & $git -c safe.directory=* -C $Path rev-parse --show-toplevel 2>$null
|
|
311
|
+
if ($LASTEXITCODE -eq 0 -and $top) {
|
|
312
|
+
$topPath = ("$top").Replace('/', '\').TrimEnd('\')
|
|
313
|
+
if ($topPath -ieq ((Resolve-Path -LiteralPath $Path).ProviderPath.TrimEnd('\'))) { $isRepo = $true }
|
|
314
|
+
else { $r.error = "$Path is a subfolder of the repository $topPath"; return $r }
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
$r.isRepo = $isRepo
|
|
318
|
+
if ($isRepo) {
|
|
319
|
+
$bundle = Join-Path $tmp 'repo.bundle'
|
|
320
|
+
& $git -c safe.directory=* -C $Path bundle create $bundle --all 2>&1 | Out-Null
|
|
321
|
+
if ($LASTEXITCODE -ne 0) { $r.error = "git bundle failed with exit $LASTEXITCODE"; return $r }
|
|
322
|
+
$r.bundle = $bundle
|
|
323
|
+
$h = & $git -c safe.directory=* -C $Path symbolic-ref --short HEAD 2>$null
|
|
324
|
+
if ($LASTEXITCODE -eq 0) { $r.head = "$h" }
|
|
325
|
+
$r.headSha = "$(& $git -c safe.directory=* -C $Path rev-parse HEAD)"
|
|
326
|
+
# A bare repository has no working tree, so nothing can be uncommitted.
|
|
327
|
+
if (-not $isBare) { $r.dirty = @(& $git -c safe.directory=* -C $Path status --porcelain --untracked-files=all) }
|
|
328
|
+
if ($WantDirty -and $r.dirty.Count -gt 0) {
|
|
329
|
+
$r.changed = @(& $git -c safe.directory=* -C $Path ls-files -m -o --exclude-standard)
|
|
330
|
+
$r.deleted = @(& $git -c safe.directory=* -C $Path ls-files -d)
|
|
331
|
+
$dd = Join-Path $tmp 'dirty'
|
|
332
|
+
New-Item -ItemType Directory -Path $dd -ErrorAction Stop | Out-Null
|
|
333
|
+
foreach ($rel in $r.changed) {
|
|
334
|
+
$src = Join-Path $Path $rel
|
|
335
|
+
if (-not (Test-Path -LiteralPath $src -PathType Leaf)) { continue }
|
|
336
|
+
$dst = Join-Path $dd $rel
|
|
337
|
+
New-Item -ItemType Directory -Force -Path (Split-Path $dst -Parent) -ErrorAction Stop | Out-Null
|
|
338
|
+
Copy-Item -LiteralPath $src -Destination $dst -Force -ErrorAction Stop
|
|
339
|
+
}
|
|
340
|
+
$r.dirtyZip = Join-Path $tmp 'dirty.zip'
|
|
341
|
+
[IO.Compression.ZipFile]::CreateFromDirectory($dd, $r.dirtyZip)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
$tree = Join-Path $tmp 'tree'
|
|
346
|
+
& robocopy $Path $tree /E /R:1 /W:1 /NFL /NDL /NJH /NJS /NP /XJ /XD @JunkDirs | Out-Null
|
|
347
|
+
if ($LASTEXITCODE -ge 8) { $r.error = "robocopy failed with exit $LASTEXITCODE"; return $r }
|
|
348
|
+
$r.treeZip = Join-Path $tmp 'tree.zip'
|
|
349
|
+
# ZipFile, not Compress-Archive: Compress-Archive's wildcard skips hidden files (.env, .gitignore).
|
|
350
|
+
[IO.Compression.ZipFile]::CreateFromDirectory($tree, $r.treeZip)
|
|
351
|
+
}
|
|
352
|
+
} catch { $r.error = "remote step failed: $($_.Exception.Message)" }
|
|
353
|
+
return $r
|
|
354
|
+
}
|
|
355
|
+
if (-not $remote.exists) { throw "Folder not found on ${ComputerName}: $remotePath" }
|
|
356
|
+
if ($remote.error) { throw "Remote read failed on ${ComputerName}: $($remote.error)" }
|
|
357
|
+
New-Item -ItemType Directory -Force -Path $work | Out-Null
|
|
358
|
+
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
359
|
+
if ($remote.isRepo) {
|
|
360
|
+
$localBundle = Join-Path $work 'repo.bundle'
|
|
361
|
+
Copy-Item -FromSession $session -Path $remote.bundle -Destination $localBundle
|
|
362
|
+
Invoke-PortGit @('bundle', 'verify', $localBundle) | Out-Null
|
|
363
|
+
Invoke-PortGit @('clone', '-q', '--mirror', $localBundle, $mirror) | Out-Null
|
|
364
|
+
$manifest.hadGitHistory = $true
|
|
365
|
+
$manifest.sourceHead = if ($remote.head) { $remote.head } else { $remote.headSha }
|
|
366
|
+
$manifest.dirtyFiles = @($remote.dirty)
|
|
367
|
+
if ($manifest.dirtyFiles.Count -gt 0) {
|
|
368
|
+
if ($IncludeUncommitted -and $remote.dirtyZip) {
|
|
369
|
+
$localDirtyZip = Join-Path $work 'dirty.zip'
|
|
370
|
+
Copy-Item -FromSession $session -Path $remote.dirtyZip -Destination $localDirtyZip
|
|
371
|
+
$dirtyRoot = Join-Path $work 'dirty'
|
|
372
|
+
[IO.Compression.ZipFile]::ExtractToDirectory($localDirtyZip, $dirtyRoot)
|
|
373
|
+
Add-Snapshot $dirtyRoot @($remote.changed) @($remote.deleted) $remote.headSha
|
|
374
|
+
}
|
|
375
|
+
elseif (-not $IncludeUncommitted) {
|
|
376
|
+
$manifest.warnings += "The source has $($manifest.dirtyFiles.Count) uncommitted change(s). They are NOT included. Re-run with -IncludeUncommitted to keep them on a separate branch."
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
$localZip = Join-Path $work 'tree.zip'
|
|
382
|
+
Copy-Item -FromSession $session -Path $remote.treeZip -Destination $localZip
|
|
383
|
+
$tree = Join-Path $work 'tree'
|
|
384
|
+
[IO.Compression.ZipFile]::ExtractToDirectory($localZip, $tree)
|
|
385
|
+
Import-PlainTree $tree
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
finally {
|
|
389
|
+
if ($session) {
|
|
390
|
+
if ($remote -and $remote.tmp) {
|
|
391
|
+
Invoke-Command -Session $session -ArgumentList $remote.tmp -ScriptBlock { param($t) Remove-Item -LiteralPath $t -Recurse -Force -ErrorAction SilentlyContinue }
|
|
392
|
+
}
|
|
393
|
+
Remove-PSSession $session
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
# ---- 3. Normalise the mirror to heads and tags only ---------------------------------------
|
|
400
|
+
$allRefs = @(Invoke-PortGit @('-C', $mirror, 'for-each-ref', '--format=%(refname)'))
|
|
401
|
+
$headNames = @($allRefs | Where-Object { $_ -like 'refs/heads/*' } | ForEach-Object { $_.Substring(11) })
|
|
402
|
+
foreach ($ref in ($allRefs | Where-Object { $_ -like 'refs/remotes/*' })) {
|
|
403
|
+
$parts = $ref.Substring(13) -split '/', 2
|
|
404
|
+
if ($parts.Count -lt 2 -or $parts[1] -eq 'HEAD') { continue }
|
|
405
|
+
if ($headNames -contains $parts[1]) { continue }
|
|
406
|
+
if ($PromoteRemoteBranches -and $parts[0] -eq $PromoteRemoteBranches) {
|
|
407
|
+
Invoke-PortGit @('-C', $mirror, 'update-ref', "refs/heads/$($parts[1])", $ref) | Out-Null
|
|
408
|
+
$manifest.promotedBranches += $parts[1]
|
|
409
|
+
$headNames += $parts[1]
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
$manifest.remoteOnlyBranches += "$($parts[0])/$($parts[1])"
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
foreach ($ref in $allRefs) {
|
|
416
|
+
if ($ref -like 'refs/heads/*' -or $ref -like 'refs/tags/*') { continue }
|
|
417
|
+
Invoke-PortGit @('-C', $mirror, 'update-ref', '-d', $ref) | Out-Null
|
|
418
|
+
$manifest.droppedRefs += $ref
|
|
419
|
+
}
|
|
420
|
+
# A mirror clone keeps a push-capable remote pointing at the SOURCE. Remove it, so no command
|
|
421
|
+
# run in this folder can ever write back to where the app came from.
|
|
422
|
+
$remotes = @(Invoke-PortGit @('-C', $mirror, 'remote'))
|
|
423
|
+
foreach ($r in $remotes) { if ($r) { Invoke-PortGit @('-C', $mirror, 'remote', 'remove', $r) | Out-Null } }
|
|
424
|
+
|
|
425
|
+
$heads = @(Invoke-PortGit @('-C', $mirror, 'for-each-ref', '--format=%(refname:short) %(objectname)', 'refs/heads'))
|
|
426
|
+
$tags = @(Invoke-PortGit @('-C', $mirror, 'for-each-ref', '--format=%(refname:short) %(objectname)', 'refs/tags'))
|
|
427
|
+
$manifest.heads = $heads
|
|
428
|
+
$manifest.tags = $tags
|
|
429
|
+
if ($heads.Count -eq 0) { throw "The source produced no branches. Nothing to port." }
|
|
430
|
+
|
|
431
|
+
$symHead = Invoke-PortGit -AllowFailure @('-C', $mirror, 'symbolic-ref', '--short', 'HEAD')
|
|
432
|
+
$def = if ($symHead.ExitCode -eq 0) { $symHead.Output -join '' } else { $null }
|
|
433
|
+
if (-not $def -or -not ($headNames -contains $def)) {
|
|
434
|
+
$def = @('main', 'master', 'develop') | Where-Object { $headNames -contains $_ } | Select-Object -First 1
|
|
435
|
+
if (-not $def) { $def = $headNames[0] }
|
|
436
|
+
Invoke-PortGit @('-C', $mirror, 'symbolic-ref', 'HEAD', "refs/heads/$def") | Out-Null
|
|
437
|
+
}
|
|
438
|
+
$manifest.defaultBranch = $def
|
|
439
|
+
|
|
440
|
+
if ($manifest.remoteOnlyBranches.Count -gt 0) {
|
|
441
|
+
$manifest.warnings += "$($manifest.remoteOnlyBranches.Count) branch(es) exist only as remote-tracking refs in the source and are NOT included: $($manifest.remoteOnlyBranches -join ', '). If this checkout is the only copy, re-run with -PromoteRemoteBranches <remote>. If a server holds them, port from the server URL instead."
|
|
442
|
+
}
|
|
443
|
+
if ($manifest.excludedSecretFiles.Count -gt 0) {
|
|
444
|
+
$manifest.warnings += "Left out $($manifest.excludedSecretFiles.Count) likely secret file(s): $($manifest.excludedSecretFiles -join ', '). They stay only in the source. Rotate any real credential they hold."
|
|
445
|
+
}
|
|
446
|
+
if ($manifest.suspectDirs.Count -gt 0) {
|
|
447
|
+
$manifest.warnings += "Imported folders that are often build output or data: $($manifest.suspectDirs -join ', '). Check they belong in the repository."
|
|
448
|
+
}
|
|
449
|
+
if ($manifest.largeFiles.Count -gt 0) {
|
|
450
|
+
$manifest.warnings += "$($manifest.largeFiles.Count) file(s) over 50 MB. GitHub rejects files over 100 MB."
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
$manifest.status = 'staged'
|
|
454
|
+
Save-Manifest
|
|
455
|
+
|
|
456
|
+
Write-PortOk "Staged $Name at $mirror"
|
|
457
|
+
Write-Host (" kind={0} history={1} branches={2} tags={3} default={4} dropped-refs={5}" -f $kind, $manifest.hadGitHistory, $heads.Count, $tags.Count, $def, $manifest.droppedRefs.Count)
|
|
458
|
+
foreach ($w in $manifest.warnings) { Write-PortWarn $w }
|
|
459
|
+
Write-Host " manifest: $(Join-Path $stage 'manifest.json')"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Shared helpers for the port-to-github scripts. Dot-source this file.
|
|
2
|
+
# ASCII only: these scripts may be read by Windows PowerShell 5.1.
|
|
3
|
+
|
|
4
|
+
Set-StrictMode -Version 2.0
|
|
5
|
+
|
|
6
|
+
function Write-PortStep([string]$Message) {
|
|
7
|
+
Write-Host "==> $Message" -ForegroundColor Cyan
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function Write-PortWarn([string]$Message) {
|
|
11
|
+
Write-Host "[WARN] $Message" -ForegroundColor Yellow
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function Write-PortFail([string]$Message) {
|
|
15
|
+
Write-Host "[FAIL] $Message" -ForegroundColor Red
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Write-PortOk([string]$Message) {
|
|
19
|
+
Write-Host "[OK] $Message" -ForegroundColor Green
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Run git and fail loud on a non-zero exit. Returns stdout lines.
|
|
23
|
+
# safe.directory=* lets us READ a repository another account owns (a UNC share,
|
|
24
|
+
# a service-owned prod checkout). -c is forwarded to git's subprocesses through
|
|
25
|
+
# GIT_CONFIG_PARAMETERS, so upload-pack sees it too.
|
|
26
|
+
function Invoke-PortGit {
|
|
27
|
+
param(
|
|
28
|
+
[Parameter(Mandatory)][string[]]$Arguments,
|
|
29
|
+
[switch]$AllowFailure
|
|
30
|
+
)
|
|
31
|
+
$all = @('-c', 'safe.directory=*') + $Arguments
|
|
32
|
+
$out = & git @all 2>&1
|
|
33
|
+
$code = $LASTEXITCODE
|
|
34
|
+
$text = @($out | ForEach-Object { "$_" })
|
|
35
|
+
if ($code -ne 0 -and -not $AllowFailure) {
|
|
36
|
+
throw "git $($Arguments -join ' ') failed with exit $code`n$($text -join "`n")"
|
|
37
|
+
}
|
|
38
|
+
if ($AllowFailure) {
|
|
39
|
+
return [pscustomobject]@{ ExitCode = $code; Output = $text }
|
|
40
|
+
}
|
|
41
|
+
return $text
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Read defaults from a project config file. Order: -ConfigPath, $env:PORT_TO_GITHUB_CONFIG,
|
|
45
|
+
# .caddis/port-to-github.json in the current git repository. Missing file = empty config.
|
|
46
|
+
function Get-PortConfig([string]$ConfigPath) {
|
|
47
|
+
$candidates = @()
|
|
48
|
+
if ($ConfigPath) { $candidates += $ConfigPath }
|
|
49
|
+
if ($env:PORT_TO_GITHUB_CONFIG) { $candidates += $env:PORT_TO_GITHUB_CONFIG }
|
|
50
|
+
$top = & git rev-parse --show-toplevel 2>$null
|
|
51
|
+
if ($LASTEXITCODE -eq 0 -and $top) {
|
|
52
|
+
$candidates += (Join-Path $top '.caddis/port-to-github.json')
|
|
53
|
+
}
|
|
54
|
+
foreach ($c in $candidates) {
|
|
55
|
+
if ($c -and (Test-Path -LiteralPath $c)) {
|
|
56
|
+
$cfg = Get-Content -LiteralPath $c -Raw | ConvertFrom-Json
|
|
57
|
+
$cfg | Add-Member -NotePropertyName _path -NotePropertyValue (Resolve-Path -LiteralPath $c).Path -Force
|
|
58
|
+
return $cfg
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [pscustomobject]@{ _path = $null }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Get-PortConfigValue($Config, [string]$Name, $Default) {
|
|
65
|
+
if ($null -ne $Config -and $Config.PSObject.Properties.Name -contains $Name -and $null -ne $Config.$Name -and "$($Config.$Name)" -ne '') {
|
|
66
|
+
return $Config.$Name
|
|
67
|
+
}
|
|
68
|
+
return $Default
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Get-PortStagingRoot($Config, [string]$Override) {
|
|
72
|
+
if ($Override) { $root = $Override }
|
|
73
|
+
else { $root = Get-PortConfigValue $Config 'stagingRoot' (Join-Path ([IO.Path]::GetTempPath()) 'port-to-github') }
|
|
74
|
+
$root = [Environment]::ExpandEnvironmentVariables($root)
|
|
75
|
+
New-Item -ItemType Directory -Force -Path $root | Out-Null
|
|
76
|
+
return (Resolve-Path -LiteralPath $root).Path
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Remove "user:secret@" from a URL. Returns @{ Url; HadCredential }.
|
|
80
|
+
function Remove-UrlCredential([string]$Url) {
|
|
81
|
+
if ($Url -match '^(?<scheme>[a-zA-Z][a-zA-Z0-9+.-]*://)(?<userinfo>[^@/]+)@(?<rest>.*)$') {
|
|
82
|
+
return [pscustomobject]@{ Url = "$($Matches.scheme)$($Matches.rest)"; HadCredential = ($Matches.userinfo -match ':') }
|
|
83
|
+
}
|
|
84
|
+
return [pscustomobject]@{ Url = $Url; HadCredential = $false }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Directories that are never source, skipped when importing a plain folder (no .git).
|
|
88
|
+
# Only unambiguous ones: 'dist', 'build', 'bin' can hold real source in some projects,
|
|
89
|
+
# so they are imported and REPORTED instead (see $PortSuspectDirs).
|
|
90
|
+
$script:PortJunkDirs = @(
|
|
91
|
+
'node_modules', '.venv', 'venv', '__pycache__', '.pytest_cache', '.mypy_cache',
|
|
92
|
+
'.ruff_cache', '.tox', '.next', '.nuxt', '.parcel-cache', '.turbo', 'htmlcov', '.git'
|
|
93
|
+
)
|
|
94
|
+
$script:PortSuspectDirs = @('dist', 'build', 'bin', 'obj', 'out', 'coverage', 'logs', 'data')
|
|
95
|
+
|
|
96
|
+
# File-name patterns that usually hold secrets.
|
|
97
|
+
$script:PortSecretNamePatterns = @(
|
|
98
|
+
'^\.env$', '^\.env\..+', '\.pem$', '\.key$', '\.pfx$', '\.p12$', '^id_rsa', '^id_ed25519',
|
|
99
|
+
'^credentials(\..+)?$', '\.kdbx$', '^\.netrc$', '^\.git-credentials$', '^\.npmrc$', '^\.pypirc$'
|
|
100
|
+
)
|
|
101
|
+
# .env.example / .env.sample / .env.template are documentation, not secrets.
|
|
102
|
+
$script:PortSecretNameAllow = @('\.example$', '\.sample$', '\.template$', '\.dist$')
|
|
103
|
+
|
|
104
|
+
# Content patterns that usually are a real credential.
|
|
105
|
+
$script:PortSecretContentPatterns = @(
|
|
106
|
+
'gh[pousr]_[A-Za-z0-9]{36,}',
|
|
107
|
+
'github_pat_[A-Za-z0-9_]{40,}',
|
|
108
|
+
'AKIA[0-9A-Z]{16}',
|
|
109
|
+
'-----BEGIN [A-Z ]*PRIVATE KEY-----',
|
|
110
|
+
'xox[baprs]-[A-Za-z0-9-]{10,}',
|
|
111
|
+
'(?i)(password|passwd|pwd|secret|api[_-]?key|token)\s*[:=]\s*[''"][^''"\s]{8,}[''"]',
|
|
112
|
+
'(?i)(password|pwd)=[^;\s]{6,};'
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
function Test-PortSecretName([string]$FileName) {
|
|
116
|
+
foreach ($a in $script:PortSecretNameAllow) { if ($FileName -match $a) { return $false } }
|
|
117
|
+
foreach ($p in $script:PortSecretNamePatterns) { if ($FileName -match $p) { return $true } }
|
|
118
|
+
return $false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function Test-PortSecretContent([string]$Text) {
|
|
122
|
+
foreach ($p in $script:PortSecretContentPatterns) {
|
|
123
|
+
if ($Text -match $p) { return $p }
|
|
124
|
+
}
|
|
125
|
+
return $null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ConvertTo-PortJsonFile($Object, [string]$Path) {
|
|
129
|
+
$Object | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $Path -Encoding UTF8
|
|
130
|
+
}
|