git-clone-resume 0.1.1
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/README.md +167 -0
- package/gcr.cmd +4 -0
- package/git-clone-resume.cmd +22 -0
- package/git-clone-resume.ps1 +1421 -0
- package/git-clone-resume.tui.ps1 +1718 -0
- package/package.json +29 -0
|
@@ -0,0 +1,1718 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
# Terminal UI for git-clone-resume. Dot-sourced by git-clone-resume.ps1.
|
|
3
|
+
# PowerShell 5.1 compatible. Interactive host -> full-screen dashboard;
|
|
4
|
+
# redirected/CI -> caller keeps the original CLI.
|
|
5
|
+
|
|
6
|
+
Set-StrictMode -Version Latest
|
|
7
|
+
|
|
8
|
+
$script:GcrTui = $null
|
|
9
|
+
$script:GcrEsc = [char]27
|
|
10
|
+
$script:GcrReset = "$([char]27)[0m"
|
|
11
|
+
$script:GcrCurrentProc = $null
|
|
12
|
+
$script:GcrOrigOutMode = $null
|
|
13
|
+
$script:GcrOrigInMode = $null
|
|
14
|
+
$script:GcrOrigTitle = $null
|
|
15
|
+
$script:GcrOrigCursor = $true
|
|
16
|
+
$script:GcrOrigTreatCtrlC = $false
|
|
17
|
+
$script:GcrNativeReady = $false
|
|
18
|
+
|
|
19
|
+
function Get-GcrCharWidth {
|
|
20
|
+
param([char]$Ch)
|
|
21
|
+
$code = [int]$Ch
|
|
22
|
+
if ($code -le 31 -or $code -eq 127) { return 0 }
|
|
23
|
+
if ($code -lt 127) { return 1 }
|
|
24
|
+
if ($code -ge 0x1100 -and (
|
|
25
|
+
$code -le 0x115F -or
|
|
26
|
+
$code -eq 0x2329 -or $code -eq 0x232A -or
|
|
27
|
+
($code -ge 0x2E80 -and $code -le 0xA4CF -and $code -ne 0x303F) -or
|
|
28
|
+
($code -ge 0xAC00 -and $code -le 0xD7A3) -or
|
|
29
|
+
($code -ge 0xF900 -and $code -le 0xFAFF) -or
|
|
30
|
+
($code -ge 0xFE10 -and $code -le 0xFE19) -or
|
|
31
|
+
($code -ge 0xFE30 -and $code -le 0xFE6F) -or
|
|
32
|
+
($code -ge 0xFF00 -and $code -le 0xFF60) -or
|
|
33
|
+
($code -ge 0xFFE0 -and $code -le 0xFFE6)
|
|
34
|
+
)) { return 2 }
|
|
35
|
+
return 1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Get-GcrDisplayWidth {
|
|
39
|
+
param([string]$Text)
|
|
40
|
+
if ([string]::IsNullOrEmpty($Text)) { return 0 }
|
|
41
|
+
$plain = [regex]::Replace($Text, [char]27 + '\[[0-9;?]*[ -/]*[@-~]', "")
|
|
42
|
+
$w = 0
|
|
43
|
+
foreach ($ch in $plain.ToCharArray()) { $w += Get-GcrCharWidth $ch }
|
|
44
|
+
return $w
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function Truncate-GcrDisplay {
|
|
48
|
+
param([string]$Text, [int]$Width)
|
|
49
|
+
if ($Width -le 0) { return "" }
|
|
50
|
+
if ([string]::IsNullOrEmpty($Text)) { return "" }
|
|
51
|
+
if ((Get-GcrDisplayWidth $Text) -le $Width) { return $Text }
|
|
52
|
+
$ellipsis = "..."
|
|
53
|
+
$budget = $Width - 3
|
|
54
|
+
if ($budget -lt 1) { return Truncate-GcrDisplay -Text "." -Width $Width }
|
|
55
|
+
$sb = New-Object System.Text.StringBuilder
|
|
56
|
+
$w = 0
|
|
57
|
+
foreach ($ch in $Text.ToCharArray()) {
|
|
58
|
+
$cw = Get-GcrCharWidth $ch
|
|
59
|
+
if ($w + $cw -gt $budget) { break }
|
|
60
|
+
[void]$sb.Append($ch)
|
|
61
|
+
$w += $cw
|
|
62
|
+
}
|
|
63
|
+
return $sb.ToString() + $ellipsis
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Format-GcrCell {
|
|
67
|
+
param([string]$Text, [int]$Width)
|
|
68
|
+
if ($Width -le 0) { return "" }
|
|
69
|
+
$t = Truncate-GcrDisplay -Text ([string]$Text) -Width $Width
|
|
70
|
+
$w = Get-GcrDisplayWidth $t
|
|
71
|
+
if ($w -lt $Width) { $t = $t + (" " * ($Width - $w)) }
|
|
72
|
+
return $t
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Get-GcrColor {
|
|
76
|
+
param([string]$Name)
|
|
77
|
+
$e = $script:GcrEsc
|
|
78
|
+
switch ($Name) {
|
|
79
|
+
"reset" { return "$e[0m" }
|
|
80
|
+
"bold" { return "$e[1m" }
|
|
81
|
+
"dim" { return "$e[2m" }
|
|
82
|
+
"rev" { return "$e[7m" }
|
|
83
|
+
"cyan" { return "$e[96m" }
|
|
84
|
+
"blue" { return "$e[94m" }
|
|
85
|
+
"green" { return "$e[92m" }
|
|
86
|
+
"yellow" { return "$e[93m" }
|
|
87
|
+
"red" { return "$e[91m" }
|
|
88
|
+
"white" { return "$e[97m" }
|
|
89
|
+
"gray" { return "$e[90m" }
|
|
90
|
+
"teal" { return "$e[38;2;94;234;212m" }
|
|
91
|
+
default { return "$e[0m" }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Get-GcrLevelColor {
|
|
96
|
+
param([string]$Level)
|
|
97
|
+
switch ($Level) {
|
|
98
|
+
"OK" { return Get-GcrColor "green" }
|
|
99
|
+
"STEP" { return Get-GcrColor "cyan" }
|
|
100
|
+
"WARN" { return Get-GcrColor "yellow" }
|
|
101
|
+
"ERROR" { return Get-GcrColor "red" }
|
|
102
|
+
default { return Get-GcrColor "dim" }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Test-GcrTuiAvailable {
|
|
107
|
+
if ($Host.Name -match "ISE") { return $false }
|
|
108
|
+
try {
|
|
109
|
+
if ([Console]::IsOutputRedirected) { return $false }
|
|
110
|
+
if ([Console]::IsInputRedirected) { return $false }
|
|
111
|
+
} catch { return $false }
|
|
112
|
+
try {
|
|
113
|
+
$w = [Console]::WindowWidth
|
|
114
|
+
$h = [Console]::WindowHeight
|
|
115
|
+
if ($w -lt 40 -or $h -lt 10) { return $false }
|
|
116
|
+
} catch { return $false }
|
|
117
|
+
try {
|
|
118
|
+
$null = [Console]::KeyAvailable
|
|
119
|
+
} catch { return $false }
|
|
120
|
+
return $true
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function Test-GcrTuiActive {
|
|
124
|
+
return ($null -ne $script:GcrTui -and [bool]$script:GcrTui.Active)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function Test-GcrTuiQuit {
|
|
128
|
+
return (Test-GcrTuiActive) -and [bool]$script:GcrTui.QuitRequested
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function Test-GcrTuiForceQuit {
|
|
132
|
+
return (Test-GcrTuiActive) -and [bool]$script:GcrTui.ForceQuit
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function Get-GcrNativeType {
|
|
136
|
+
if (-not ("GitCloneResume.Native" -as [type])) {
|
|
137
|
+
Add-Type -Namespace GitCloneResume -Name Native -MemberDefinition @"
|
|
138
|
+
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError=true)]
|
|
139
|
+
public static extern System.IntPtr GetStdHandle(int nStdHandle);
|
|
140
|
+
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError=true)]
|
|
141
|
+
public static extern bool GetConsoleMode(System.IntPtr hConsoleHandle, out uint lpMode);
|
|
142
|
+
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError=true)]
|
|
143
|
+
public static extern bool SetConsoleMode(System.IntPtr hConsoleHandle, uint dwMode);
|
|
144
|
+
"@
|
|
145
|
+
}
|
|
146
|
+
return [GitCloneResume.Native]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function Enable-GcrVt {
|
|
150
|
+
try {
|
|
151
|
+
$native = Get-GcrNativeType
|
|
152
|
+
$hOut = $native::GetStdHandle(-11)
|
|
153
|
+
$hIn = $native::GetStdHandle(-10)
|
|
154
|
+
$outMode = [uint32]0
|
|
155
|
+
$inMode = [uint32]0
|
|
156
|
+
if ($native::GetConsoleMode($hOut, [ref]$outMode)) {
|
|
157
|
+
$script:GcrOrigOutMode = $outMode
|
|
158
|
+
$vt = [uint32]0x0004
|
|
159
|
+
$processed = [uint32]0x0001
|
|
160
|
+
$wrap = [uint32]0x0002
|
|
161
|
+
$disableNl = [uint32]0x0008
|
|
162
|
+
$newOut = ([uint32]($outMode -bor $processed -bor $vt -bor $disableNl)) -band (-bnot $wrap)
|
|
163
|
+
[void]$native::SetConsoleMode($hOut, $newOut)
|
|
164
|
+
}
|
|
165
|
+
if ($native::GetConsoleMode($hIn, [ref]$inMode)) {
|
|
166
|
+
$script:GcrOrigInMode = $inMode
|
|
167
|
+
$extended = [uint32]0x0080
|
|
168
|
+
$quickEdit = [uint32]0x0040
|
|
169
|
+
$newIn = ($inMode -bor $extended) -band (-bnot $quickEdit)
|
|
170
|
+
[void]$native::SetConsoleMode($hIn, $newIn)
|
|
171
|
+
}
|
|
172
|
+
$script:GcrNativeReady = $true
|
|
173
|
+
return $true
|
|
174
|
+
} catch {
|
|
175
|
+
return $false
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function Restore-GcrVt {
|
|
180
|
+
if (-not $script:GcrNativeReady) { return }
|
|
181
|
+
try {
|
|
182
|
+
$native = Get-GcrNativeType
|
|
183
|
+
if ($null -ne $script:GcrOrigOutMode) {
|
|
184
|
+
[void]$native::SetConsoleMode($native::GetStdHandle(-11), [uint32]$script:GcrOrigOutMode)
|
|
185
|
+
}
|
|
186
|
+
if ($null -ne $script:GcrOrigInMode) {
|
|
187
|
+
[void]$native::SetConsoleMode($native::GetStdHandle(-10), [uint32]$script:GcrOrigInMode)
|
|
188
|
+
}
|
|
189
|
+
} catch { }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function Sync-GcrConsoleBuffer {
|
|
193
|
+
try {
|
|
194
|
+
$w = [int][Console]::WindowWidth
|
|
195
|
+
$h = [int][Console]::WindowHeight
|
|
196
|
+
if ($w -lt 1 -or $h -lt 1) { return }
|
|
197
|
+
if ([Console]::BufferWidth -ne $w -or [Console]::BufferHeight -ne $h) {
|
|
198
|
+
[Console]::SetBufferSize($w, $h)
|
|
199
|
+
}
|
|
200
|
+
} catch { }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function Get-GcrTuiSize {
|
|
204
|
+
Sync-GcrConsoleBuffer
|
|
205
|
+
$w = 80
|
|
206
|
+
$h = 24
|
|
207
|
+
try {
|
|
208
|
+
$w = [int][Console]::WindowWidth
|
|
209
|
+
$h = [int][Console]::WindowHeight
|
|
210
|
+
} catch {
|
|
211
|
+
try {
|
|
212
|
+
$w = [int]$Host.UI.RawUI.WindowSize.Width
|
|
213
|
+
$h = [int]$Host.UI.RawUI.WindowSize.Height
|
|
214
|
+
} catch { }
|
|
215
|
+
}
|
|
216
|
+
if ($w -lt 40) { $w = 40 }
|
|
217
|
+
if ($h -lt 10) { $h = 10 }
|
|
218
|
+
# Never paint the last column: a full-width write wraps and scrolls the buffer.
|
|
219
|
+
$size = @{ W = $w; H = $h; DrawW = [Math]::Max(20, $w - 1) }
|
|
220
|
+
return $size
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function Get-GcrAsciiBox {
|
|
224
|
+
return @{
|
|
225
|
+
H = "-"
|
|
226
|
+
V = "|"
|
|
227
|
+
TL = "+"
|
|
228
|
+
TR = "+"
|
|
229
|
+
BL = "+"
|
|
230
|
+
BR = "+"
|
|
231
|
+
L = "+"
|
|
232
|
+
R = "+"
|
|
233
|
+
BarF = "#"
|
|
234
|
+
BarE = "-"
|
|
235
|
+
Pointer = ">"
|
|
236
|
+
Dot = "."
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function Get-GcrUnicodeBox {
|
|
241
|
+
return @{
|
|
242
|
+
H = [string][char]0x2500
|
|
243
|
+
V = [string][char]0x2502
|
|
244
|
+
TL = [string][char]0x256D
|
|
245
|
+
TR = [string][char]0x256E
|
|
246
|
+
BL = [string][char]0x2570
|
|
247
|
+
BR = [string][char]0x256F
|
|
248
|
+
L = [string][char]0x251C
|
|
249
|
+
R = [string][char]0x2524
|
|
250
|
+
BarF = [string][char]0x2588
|
|
251
|
+
BarE = [string][char]0x2591
|
|
252
|
+
Pointer = ">"
|
|
253
|
+
Dot = [string][char]0x00B7
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function Measure-GcrCellAdvance {
|
|
258
|
+
param([char]$Ch)
|
|
259
|
+
try {
|
|
260
|
+
$left = [Console]::CursorLeft
|
|
261
|
+
$top = [Console]::CursorTop
|
|
262
|
+
[Console]::SetCursorPosition(0, 0)
|
|
263
|
+
[Console]::Write([string]$Ch)
|
|
264
|
+
$adv = [int][Console]::CursorLeft
|
|
265
|
+
[Console]::SetCursorPosition(0, 0)
|
|
266
|
+
[Console]::Write(" ")
|
|
267
|
+
[Console]::SetCursorPosition($left, $top)
|
|
268
|
+
if ($adv -lt 1) { return 1 }
|
|
269
|
+
return $adv
|
|
270
|
+
} catch {
|
|
271
|
+
return 1
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function Resolve-GcrBox {
|
|
276
|
+
if ($env:GCR_ASCII -eq "1") { return Get-GcrAsciiBox }
|
|
277
|
+
if ($env:GCR_UNICODE -eq "1") { return Get-GcrUnicodeBox }
|
|
278
|
+
$probe = @(
|
|
279
|
+
[char]0x2500
|
|
280
|
+
[char]0x256D
|
|
281
|
+
[char]0x2588
|
|
282
|
+
[char]0x2591
|
|
283
|
+
)
|
|
284
|
+
foreach ($ch in $probe) {
|
|
285
|
+
if ((Measure-GcrCellAdvance $ch) -ge 2) { return Get-GcrAsciiBox }
|
|
286
|
+
}
|
|
287
|
+
return Get-GcrUnicodeBox
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function Initialize-GcrTuiState {
|
|
291
|
+
$box = Get-GcrAsciiBox
|
|
292
|
+
$script:GcrTui = @{
|
|
293
|
+
Active = $false
|
|
294
|
+
UseUnicode = $false
|
|
295
|
+
Box = $box
|
|
296
|
+
Width = 80
|
|
297
|
+
Height = 24
|
|
298
|
+
LastFrame = @()
|
|
299
|
+
LastFrameW = 0
|
|
300
|
+
LastFrameH = 0
|
|
301
|
+
Logs = New-Object System.Collections.ArrayList
|
|
302
|
+
LogOffset = 0
|
|
303
|
+
Phase = "idle"
|
|
304
|
+
PhaseDetail = ""
|
|
305
|
+
RepoUrl = ""
|
|
306
|
+
OutDir = ""
|
|
307
|
+
Ref = "HEAD"
|
|
308
|
+
Commit = ""
|
|
309
|
+
Resume = $false
|
|
310
|
+
Ok = 0
|
|
311
|
+
Total = 0
|
|
312
|
+
Fail = 0
|
|
313
|
+
Bytes = [int64]0
|
|
314
|
+
Rate = 0.0
|
|
315
|
+
Eta = "--:--:--"
|
|
316
|
+
CurrentFile = ""
|
|
317
|
+
GitPercent = -1
|
|
318
|
+
Paused = $false
|
|
319
|
+
QuitRequested = $false
|
|
320
|
+
ForceQuit = $false
|
|
321
|
+
Dirty = $true
|
|
322
|
+
LastDraw = [datetime]::MinValue
|
|
323
|
+
Help = $false
|
|
324
|
+
FailView = $false
|
|
325
|
+
Failures = New-Object System.Collections.ArrayList
|
|
326
|
+
StartedAt = $null
|
|
327
|
+
Status = "run"
|
|
328
|
+
ResultTitle = ""
|
|
329
|
+
ResultBody = @()
|
|
330
|
+
Tick = 0
|
|
331
|
+
Screen = "dash"
|
|
332
|
+
ErrorFlash = ""
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function Initialize-GcrTui {
|
|
337
|
+
if (Test-GcrTuiActive) { return $true }
|
|
338
|
+
if (-not (Test-GcrTuiAvailable)) { return $false }
|
|
339
|
+
Initialize-GcrTuiState
|
|
340
|
+
try { $script:GcrOrigTitle = [Console]::Title } catch { }
|
|
341
|
+
try { $script:GcrOrigCursor = [Console]::CursorVisible } catch { }
|
|
342
|
+
try { $script:GcrOrigTreatCtrlC = [Console]::TreatControlCAsInput } catch { }
|
|
343
|
+
[void](Enable-GcrVt)
|
|
344
|
+
try { [Console]::TreatControlCAsInput = $true } catch { }
|
|
345
|
+
try { [Console]::CursorVisible = $false } catch { }
|
|
346
|
+
$e = $script:GcrEsc
|
|
347
|
+
try {
|
|
348
|
+
[Console]::Write("{0}[?1049h{0}[?25l{0}[2J{0}[H" -f $e)
|
|
349
|
+
} catch { }
|
|
350
|
+
Sync-GcrConsoleBuffer
|
|
351
|
+
$box = Resolve-GcrBox
|
|
352
|
+
$script:GcrTui.Box = $box
|
|
353
|
+
$script:GcrTui.UseUnicode = ($box.H -ne "-")
|
|
354
|
+
try { [Console]::Title = "git-clone-resume" } catch { }
|
|
355
|
+
$script:GcrTui.Active = $true
|
|
356
|
+
$script:GcrTui.Dirty = $true
|
|
357
|
+
$script:GcrTui.LastFrame = @()
|
|
358
|
+
return $true
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function Close-GcrTui {
|
|
362
|
+
if ($null -eq $script:GcrTui -or -not $script:GcrTui.Active) {
|
|
363
|
+
$script:GcrTui = $null
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
$e = $script:GcrEsc
|
|
367
|
+
try { [Console]::Write("{0}]9;4;0;0{1}" -f $e, [char]7) } catch { }
|
|
368
|
+
try { [Console]::Write("{0}[?25h{0}[?1049l{0}[0m" -f $e) } catch { }
|
|
369
|
+
try { [Console]::CursorVisible = $script:GcrOrigCursor } catch {
|
|
370
|
+
try { [Console]::CursorVisible = $true } catch { }
|
|
371
|
+
}
|
|
372
|
+
try { [Console]::TreatControlCAsInput = $script:GcrOrigTreatCtrlC } catch {
|
|
373
|
+
try { [Console]::TreatControlCAsInput = $false } catch { }
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
if ($script:GcrOrigTitle) { [Console]::Title = $script:GcrOrigTitle }
|
|
377
|
+
} catch { }
|
|
378
|
+
Restore-GcrVt
|
|
379
|
+
$script:GcrTui.Active = $false
|
|
380
|
+
$script:GcrTui = $null
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function Set-GcrTuiTabProgress {
|
|
384
|
+
param([int]$Percent, [int]$State = 1)
|
|
385
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
386
|
+
if ($Percent -lt 0) { $Percent = 0 }
|
|
387
|
+
if ($Percent -gt 100) { $Percent = 100 }
|
|
388
|
+
$e = $script:GcrEsc
|
|
389
|
+
try { [Console]::Write("{0}]9;4;{1};{2}{3}" -f $e, $State, $Percent, [char]7) } catch { }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function Set-GcrTuiRepo {
|
|
393
|
+
param(
|
|
394
|
+
[string]$Url,
|
|
395
|
+
[string]$OutDir,
|
|
396
|
+
[string]$Ref,
|
|
397
|
+
[string]$Commit,
|
|
398
|
+
[switch]$Resume
|
|
399
|
+
)
|
|
400
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
401
|
+
if ($PSBoundParameters.ContainsKey("Url")) { $script:GcrTui.RepoUrl = $Url }
|
|
402
|
+
if ($PSBoundParameters.ContainsKey("OutDir")) { $script:GcrTui.OutDir = $OutDir }
|
|
403
|
+
if ($PSBoundParameters.ContainsKey("Ref")) { $script:GcrTui.Ref = $Ref }
|
|
404
|
+
if ($PSBoundParameters.ContainsKey("Commit")) { $script:GcrTui.Commit = $Commit }
|
|
405
|
+
if ($Resume) { $script:GcrTui.Resume = $true }
|
|
406
|
+
$script:GcrTui.Dirty = $true
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function Set-GcrTuiPhase {
|
|
410
|
+
param([string]$Name, [string]$Detail = "")
|
|
411
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
412
|
+
if ($Name) { $script:GcrTui.Phase = $Name }
|
|
413
|
+
if ($PSBoundParameters.ContainsKey("Detail")) { $script:GcrTui.PhaseDetail = $Detail }
|
|
414
|
+
$script:GcrTui.Dirty = $true
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function Add-GcrTuiLog {
|
|
418
|
+
param(
|
|
419
|
+
[Parameter(Mandatory = $true)][string]$Message,
|
|
420
|
+
[string]$Level = "INFO"
|
|
421
|
+
)
|
|
422
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
423
|
+
$msg = [string]$Message
|
|
424
|
+
$msg = $msg -replace "[\r\n]+", " "
|
|
425
|
+
if ($msg.Length -gt 400) { $msg = $msg.Substring(0, 397) + "..." }
|
|
426
|
+
$entry = @{
|
|
427
|
+
T = Get-Date
|
|
428
|
+
L = [string]$Level
|
|
429
|
+
M = $msg
|
|
430
|
+
}
|
|
431
|
+
[void]$script:GcrTui.Logs.Add($entry)
|
|
432
|
+
while ($script:GcrTui.Logs.Count -gt 400) {
|
|
433
|
+
$script:GcrTui.Logs.RemoveAt(0)
|
|
434
|
+
}
|
|
435
|
+
if ($script:GcrTui.LogOffset -eq 0) { $script:GcrTui.Dirty = $true }
|
|
436
|
+
else { $script:GcrTui.Dirty = $true }
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function Add-GcrTuiFailure {
|
|
440
|
+
param([string]$Path)
|
|
441
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
442
|
+
if ([string]::IsNullOrWhiteSpace($Path)) { return }
|
|
443
|
+
[void]$script:GcrTui.Failures.Add($Path)
|
|
444
|
+
while ($script:GcrTui.Failures.Count -gt 80) {
|
|
445
|
+
$script:GcrTui.Failures.RemoveAt(0)
|
|
446
|
+
}
|
|
447
|
+
$script:GcrTui.Dirty = $true
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function Add-GcrGitOutput {
|
|
451
|
+
param([string]$Text, [switch]$Progress)
|
|
452
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
453
|
+
if ([string]::IsNullOrWhiteSpace($Text)) { return }
|
|
454
|
+
$t = $Text.Trim()
|
|
455
|
+
if ($t.Length -eq 0) { return }
|
|
456
|
+
if ($t -match "(\d+)\s*%") {
|
|
457
|
+
$script:GcrTui.GitPercent = [int]$Matches[1]
|
|
458
|
+
$script:GcrTui.PhaseDetail = $t
|
|
459
|
+
$script:GcrTui.Dirty = $true
|
|
460
|
+
if ($Progress) { return }
|
|
461
|
+
if ($t -match "Receiving objects|Resolving deltas|Counting objects|Compressing objects|Enumerating") {
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if ($Progress) {
|
|
466
|
+
$script:GcrTui.PhaseDetail = $t
|
|
467
|
+
$script:GcrTui.Dirty = $true
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
Add-GcrTuiLog -Level "INFO" -Message $t
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function Receive-GcrGitBytes {
|
|
474
|
+
param(
|
|
475
|
+
[byte[]]$Buffer,
|
|
476
|
+
[int]$Count,
|
|
477
|
+
[System.Text.StringBuilder]$Carry,
|
|
478
|
+
[switch]$IsStdErr
|
|
479
|
+
)
|
|
480
|
+
if ($Count -le 0 -or $null -eq $Carry) { return }
|
|
481
|
+
$text = [System.Text.Encoding]::UTF8.GetString($Buffer, 0, $Count)
|
|
482
|
+
[void]$Carry.Append($text)
|
|
483
|
+
$s = $Carry.ToString()
|
|
484
|
+
$Carry.Length = 0
|
|
485
|
+
$acc = New-Object System.Text.StringBuilder
|
|
486
|
+
foreach ($ch in $s.ToCharArray()) {
|
|
487
|
+
if ($ch -eq [char]13) {
|
|
488
|
+
if ($IsStdErr) { Add-GcrGitOutput -Text $acc.ToString() -Progress }
|
|
489
|
+
[void]$acc.Clear()
|
|
490
|
+
} elseif ($ch -eq [char]10) {
|
|
491
|
+
if ($IsStdErr) { Add-GcrGitOutput -Text $acc.ToString() }
|
|
492
|
+
[void]$acc.Clear()
|
|
493
|
+
} else {
|
|
494
|
+
[void]$acc.Append($ch)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if ($acc.Length -gt 0) { [void]$Carry.Append($acc.ToString()) }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function Update-GcrTuiProgress {
|
|
501
|
+
param(
|
|
502
|
+
[int]$OkCount,
|
|
503
|
+
[int]$TotalCount,
|
|
504
|
+
[int]$FailCount,
|
|
505
|
+
[int64]$DoneBytes,
|
|
506
|
+
[double]$Rate,
|
|
507
|
+
[string]$Eta,
|
|
508
|
+
[string]$CurrentFile
|
|
509
|
+
)
|
|
510
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
511
|
+
$script:GcrTui.Ok = $OkCount
|
|
512
|
+
$script:GcrTui.Total = $TotalCount
|
|
513
|
+
$script:GcrTui.Fail = $FailCount
|
|
514
|
+
$script:GcrTui.Bytes = $DoneBytes
|
|
515
|
+
$script:GcrTui.Rate = $Rate
|
|
516
|
+
if ($Eta) { $script:GcrTui.Eta = $Eta }
|
|
517
|
+
if ($PSBoundParameters.ContainsKey("CurrentFile")) { $script:GcrTui.CurrentFile = $CurrentFile }
|
|
518
|
+
$script:GcrTui.Dirty = $true
|
|
519
|
+
$pct = 0
|
|
520
|
+
if ($TotalCount -gt 0) { $pct = [int][Math]::Round(100.0 * $OkCount / $TotalCount) }
|
|
521
|
+
$state = 1
|
|
522
|
+
if ($FailCount -gt 0) { $state = 2 }
|
|
523
|
+
if ($script:GcrTui.Phase -eq "fetch" -and $script:GcrTui.GitPercent -ge 0) {
|
|
524
|
+
$pct = $script:GcrTui.GitPercent
|
|
525
|
+
$state = 1
|
|
526
|
+
}
|
|
527
|
+
Set-GcrTuiTabProgress -Percent $pct -State $state
|
|
528
|
+
try {
|
|
529
|
+
$short = Truncate-GcrDisplay -Text $script:GcrTui.RepoUrl -Width 40
|
|
530
|
+
[Console]::Title = ("git-clone-resume {0}% {1}" -f $pct, $short)
|
|
531
|
+
} catch { }
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function Get-GcrHistoryPath {
|
|
535
|
+
$root = $env:LOCALAPPDATA
|
|
536
|
+
if ([string]::IsNullOrWhiteSpace($root)) { $root = $env:USERPROFILE }
|
|
537
|
+
if ([string]::IsNullOrWhiteSpace($root)) { $root = $env:HOME }
|
|
538
|
+
if ([string]::IsNullOrWhiteSpace($root)) { $root = [Environment]::GetFolderPath("ApplicationData") }
|
|
539
|
+
return (Join-Path (Join-Path $root "git-clone-resume") "history.json")
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function Get-GcrHistory {
|
|
543
|
+
$path = Get-GcrHistoryPath
|
|
544
|
+
if (-not (Test-Path -LiteralPath $path)) { return @() }
|
|
545
|
+
try {
|
|
546
|
+
$raw = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8)
|
|
547
|
+
if ([string]::IsNullOrWhiteSpace($raw)) { return @() }
|
|
548
|
+
$parsed = $raw | ConvertFrom-Json
|
|
549
|
+
return @($parsed)
|
|
550
|
+
} catch {
|
|
551
|
+
return @()
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function Get-GcrHistoryLast {
|
|
556
|
+
$items = @(Get-GcrHistory)
|
|
557
|
+
if ($items.Count -eq 0) { return $null }
|
|
558
|
+
$partial = @($items | Where-Object { $_.status -eq "partial" -or $_.status -eq "running" -or $_.status -eq "failed" })
|
|
559
|
+
if ($partial.Count -gt 0) { return $partial[0] }
|
|
560
|
+
return $items[0]
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function Save-GcrHistory {
|
|
564
|
+
param(
|
|
565
|
+
[string]$Url,
|
|
566
|
+
[string]$OutDir,
|
|
567
|
+
[string]$Ref,
|
|
568
|
+
[string]$Commit,
|
|
569
|
+
[string]$Status,
|
|
570
|
+
[int]$Ok = 0,
|
|
571
|
+
[int]$Total = 0,
|
|
572
|
+
[int]$Fail = 0
|
|
573
|
+
)
|
|
574
|
+
if ([string]::IsNullOrWhiteSpace($OutDir) -and [string]::IsNullOrWhiteSpace($Url)) { return }
|
|
575
|
+
$items = New-Object System.Collections.ArrayList
|
|
576
|
+
foreach ($it in @(Get-GcrHistory)) {
|
|
577
|
+
$sameDir = ($it.outDir -and $OutDir -and ([string]$it.outDir -eq $OutDir))
|
|
578
|
+
$sameUrl = ($it.url -and $Url -and ([string]$it.url -eq $Url) -and -not $OutDir)
|
|
579
|
+
if (-not $sameDir -and -not $sameUrl) { [void]$items.Add($it) }
|
|
580
|
+
}
|
|
581
|
+
$entry = @{
|
|
582
|
+
url = $Url
|
|
583
|
+
outDir = $OutDir
|
|
584
|
+
ref = $Ref
|
|
585
|
+
commit = $Commit
|
|
586
|
+
status = $Status
|
|
587
|
+
ok = $Ok
|
|
588
|
+
total = $Total
|
|
589
|
+
fail = $Fail
|
|
590
|
+
updated = (Get-Date).ToString("o")
|
|
591
|
+
}
|
|
592
|
+
[void]$items.Insert(0, $entry)
|
|
593
|
+
while ($items.Count -gt 25) { [void]$items.RemoveAt($items.Count - 1) }
|
|
594
|
+
$path = Get-GcrHistoryPath
|
|
595
|
+
$dir = Split-Path -Parent $path
|
|
596
|
+
try {
|
|
597
|
+
if ($dir -and -not (Test-Path -LiteralPath $dir)) {
|
|
598
|
+
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
599
|
+
}
|
|
600
|
+
$json = ConvertTo-Json -InputObject @($items.ToArray()) -Depth 5
|
|
601
|
+
[System.IO.File]::WriteAllText($path, $json, (New-Object System.Text.UTF8Encoding $false))
|
|
602
|
+
} catch { }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function Format-GcrAgo {
|
|
606
|
+
param($When)
|
|
607
|
+
if ($null -eq $When) { return "" }
|
|
608
|
+
try {
|
|
609
|
+
$dt = [datetime]$When
|
|
610
|
+
} catch { return "" }
|
|
611
|
+
$d = (Get-Date) - $dt
|
|
612
|
+
if ($d.TotalSeconds -lt 60) { return "刚刚" }
|
|
613
|
+
if ($d.TotalMinutes -lt 60) { return ("{0} 分钟前" -f [int]$d.TotalMinutes) }
|
|
614
|
+
if ($d.TotalHours -lt 24) { return ("{0} 小时前" -f [int]$d.TotalHours) }
|
|
615
|
+
return ("{0} 天前" -f [int]$d.TotalDays)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function Format-GcrPhaseLabel {
|
|
619
|
+
param([string]$Phase)
|
|
620
|
+
switch ($Phase) {
|
|
621
|
+
"idle" { return "就绪" }
|
|
622
|
+
"wizard" { return "设置" }
|
|
623
|
+
"init" { return "初始化仓库" }
|
|
624
|
+
"fetch" { return "拉取元数据" }
|
|
625
|
+
"list" { return "枚举文件树" }
|
|
626
|
+
"scan" { return "扫描已有文件" }
|
|
627
|
+
"download"{ return "下载文件" }
|
|
628
|
+
"repair" { return "修复 git index" }
|
|
629
|
+
"done" { return "完成" }
|
|
630
|
+
"error" { return "出错" }
|
|
631
|
+
default { return $Phase }
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function New-GcrBar {
|
|
636
|
+
param([int]$Width, [double]$Ratio, [switch]$Indeterminate, [int]$Tick)
|
|
637
|
+
$box = $script:GcrTui.Box
|
|
638
|
+
if ($Width -lt 3) { return "" }
|
|
639
|
+
if ($Indeterminate) {
|
|
640
|
+
$pos = 0
|
|
641
|
+
if ($Width -gt 0) { $pos = [Math]::Abs($Tick) % $Width }
|
|
642
|
+
$sb = New-Object System.Text.StringBuilder
|
|
643
|
+
for ($i = 0; $i -lt $Width; $i++) {
|
|
644
|
+
$d = [Math]::Abs($i - $pos)
|
|
645
|
+
if ($d -le 2) { [void]$sb.Append($box.BarF) } else { [void]$sb.Append($box.BarE) }
|
|
646
|
+
}
|
|
647
|
+
return $sb.ToString()
|
|
648
|
+
}
|
|
649
|
+
if ($Ratio -lt 0) { $Ratio = 0 }
|
|
650
|
+
if ($Ratio -gt 1) { $Ratio = 1 }
|
|
651
|
+
$filled = [int][Math]::Round($Width * $Ratio)
|
|
652
|
+
if ($filled -gt $Width) { $filled = $Width }
|
|
653
|
+
return (($box.BarF * $filled) + ($box.BarE * ($Width - $filled)))
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function Out-GcrFrame {
|
|
657
|
+
param([string[]]$Lines)
|
|
658
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
659
|
+
$e = $script:GcrEsc
|
|
660
|
+
$size = Get-GcrTuiSize
|
|
661
|
+
$h = [int]$size.H
|
|
662
|
+
if ($h -lt 1) { return }
|
|
663
|
+
$rows = New-Object System.Collections.Generic.List[string]
|
|
664
|
+
$nIn = 0
|
|
665
|
+
if ($null -ne $Lines) { $nIn = @($Lines).Count }
|
|
666
|
+
for ($i = 0; $i -lt $h; $i++) {
|
|
667
|
+
if ($i -lt $nIn -and $null -ne $Lines[$i]) { [void]$rows.Add([string]$Lines[$i]) }
|
|
668
|
+
else { [void]$rows.Add("") }
|
|
669
|
+
}
|
|
670
|
+
$prev = @()
|
|
671
|
+
if ($null -ne $script:GcrTui.LastFrame) { $prev = @($script:GcrTui.LastFrame) }
|
|
672
|
+
$full = $true
|
|
673
|
+
if ($prev.Count -eq $h -and [int]$script:GcrTui.LastFrameW -eq [int]$size.W -and [int]$script:GcrTui.LastFrameH -eq $h) {
|
|
674
|
+
$full = $false
|
|
675
|
+
}
|
|
676
|
+
$sb = New-Object System.Text.StringBuilder
|
|
677
|
+
[void]$sb.Append($e).Append("[?25l")
|
|
678
|
+
for ($i = 0; $i -lt $h; $i++) {
|
|
679
|
+
$ln = $rows[$i]
|
|
680
|
+
if (-not $full -and $i -lt $prev.Count -and $ln -eq $prev[$i]) { continue }
|
|
681
|
+
[void]$sb.Append($e).Append("[").Append($i + 1).Append(";1H")
|
|
682
|
+
[void]$sb.Append($ln)
|
|
683
|
+
[void]$sb.Append($e).Append("[K")
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
[Console]::Out.Write($sb.ToString())
|
|
687
|
+
[Console]::Out.Flush()
|
|
688
|
+
} catch { }
|
|
689
|
+
try { [Console]::CursorVisible = $false } catch { }
|
|
690
|
+
$script:GcrTui.LastFrame = $rows.ToArray()
|
|
691
|
+
$script:GcrTui.LastFrameW = [int]$size.W
|
|
692
|
+
$script:GcrTui.LastFrameH = $h
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function Read-GcrTuiKey {
|
|
696
|
+
param([int]$TimeoutMs = 0)
|
|
697
|
+
try {
|
|
698
|
+
if ($TimeoutMs -le 0) {
|
|
699
|
+
if ([Console]::KeyAvailable) { return [Console]::ReadKey($true) }
|
|
700
|
+
return $null
|
|
701
|
+
}
|
|
702
|
+
$end = [Environment]::TickCount + $TimeoutMs
|
|
703
|
+
while ([Environment]::TickCount -lt $end) {
|
|
704
|
+
if ([Console]::KeyAvailable) { return [Console]::ReadKey($true) }
|
|
705
|
+
Start-Sleep -Milliseconds 20
|
|
706
|
+
}
|
|
707
|
+
} catch { }
|
|
708
|
+
return $null
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function Test-GcrCtrlKey {
|
|
712
|
+
param($Key, [string]$Code)
|
|
713
|
+
if ($null -eq $Key) { return $false }
|
|
714
|
+
$ctrl = [int][ConsoleModifiers]::Control
|
|
715
|
+
if (([int]$Key.Modifiers -band $ctrl) -eq 0) { return $false }
|
|
716
|
+
return ($Key.Key.ToString() -eq $Code)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function Invoke-GcrTuiKey {
|
|
720
|
+
param($Key)
|
|
721
|
+
if ($null -eq $Key -or -not (Test-GcrTuiActive)) { return }
|
|
722
|
+
if ($script:GcrTui.Screen -eq "result") {
|
|
723
|
+
if ($Key.Key -eq "Enter" -or $Key.Key -eq "Q" -or $Key.Key -eq "Escape") {
|
|
724
|
+
$script:GcrTui.Status = "close"
|
|
725
|
+
}
|
|
726
|
+
return
|
|
727
|
+
}
|
|
728
|
+
if ($script:GcrTui.Help) {
|
|
729
|
+
if ($Key.Key -ne "LeftArrow" -and $Key.Key -ne "RightArrow") {
|
|
730
|
+
$script:GcrTui.Help = $false
|
|
731
|
+
$script:GcrTui.Dirty = $true
|
|
732
|
+
}
|
|
733
|
+
return
|
|
734
|
+
}
|
|
735
|
+
if (Test-GcrCtrlKey -Key $Key -Code "C") {
|
|
736
|
+
Request-GcrTuiQuit
|
|
737
|
+
return
|
|
738
|
+
}
|
|
739
|
+
switch ($Key.Key.ToString()) {
|
|
740
|
+
"Q" { Request-GcrTuiQuit }
|
|
741
|
+
"P" {
|
|
742
|
+
$script:GcrTui.Paused = -not $script:GcrTui.Paused
|
|
743
|
+
$script:GcrTui.Dirty = $true
|
|
744
|
+
}
|
|
745
|
+
"Escape" {
|
|
746
|
+
if ($script:GcrTui.FailView) { $script:GcrTui.FailView = $false }
|
|
747
|
+
else { $script:GcrTui.Paused = -not $script:GcrTui.Paused }
|
|
748
|
+
$script:GcrTui.Dirty = $true
|
|
749
|
+
}
|
|
750
|
+
"Spacebar" {
|
|
751
|
+
if ($script:GcrTui.Paused) { $script:GcrTui.Paused = $false }
|
|
752
|
+
$script:GcrTui.Dirty = $true
|
|
753
|
+
}
|
|
754
|
+
"H" { $script:GcrTui.Help = $true; $script:GcrTui.Dirty = $true }
|
|
755
|
+
"F" { $script:GcrTui.FailView = -not $script:GcrTui.FailView; $script:GcrTui.Dirty = $true }
|
|
756
|
+
"UpArrow" {
|
|
757
|
+
$script:GcrTui.LogOffset = [Math]::Min($script:GcrTui.Logs.Count, $script:GcrTui.LogOffset + 1)
|
|
758
|
+
$script:GcrTui.Dirty = $true
|
|
759
|
+
}
|
|
760
|
+
"DownArrow" {
|
|
761
|
+
$script:GcrTui.LogOffset = [Math]::Max(0, $script:GcrTui.LogOffset - 1)
|
|
762
|
+
$script:GcrTui.Dirty = $true
|
|
763
|
+
}
|
|
764
|
+
"End" { $script:GcrTui.LogOffset = 0; $script:GcrTui.Dirty = $true }
|
|
765
|
+
"Home" {
|
|
766
|
+
$script:GcrTui.LogOffset = [Math]::Max(0, $script:GcrTui.Logs.Count - 1)
|
|
767
|
+
$script:GcrTui.Dirty = $true
|
|
768
|
+
}
|
|
769
|
+
"J" {
|
|
770
|
+
if ($Key.KeyChar -eq "j") {
|
|
771
|
+
$script:GcrTui.LogOffset = [Math]::Max(0, $script:GcrTui.LogOffset - 1)
|
|
772
|
+
$script:GcrTui.Dirty = $true
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
"K" {
|
|
776
|
+
if ($Key.KeyChar -eq "k") {
|
|
777
|
+
$script:GcrTui.LogOffset = [Math]::Min($script:GcrTui.Logs.Count, $script:GcrTui.LogOffset + 1)
|
|
778
|
+
$script:GcrTui.Dirty = $true
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
default {
|
|
782
|
+
if ($Key.KeyChar -eq "?") { $script:GcrTui.Help = $true; $script:GcrTui.Dirty = $true }
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function Request-GcrTuiQuit {
|
|
788
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
789
|
+
if ($script:GcrTui.QuitRequested) {
|
|
790
|
+
$script:GcrTui.ForceQuit = $true
|
|
791
|
+
if ($null -ne $script:GcrCurrentProc) {
|
|
792
|
+
try { $script:GcrCurrentProc.Kill() } catch { }
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
$script:GcrTui.QuitRequested = $true
|
|
796
|
+
$script:GcrTui.Paused = $false
|
|
797
|
+
}
|
|
798
|
+
$script:GcrTui.Dirty = $true
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function Invoke-GcrTuiTick {
|
|
802
|
+
param([switch]$Force)
|
|
803
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
804
|
+
$size = Get-GcrTuiSize
|
|
805
|
+
if ($size.W -ne $script:GcrTui.Width -or $size.H -ne $script:GcrTui.Height) {
|
|
806
|
+
$script:GcrTui.Width = $size.W
|
|
807
|
+
$script:GcrTui.Height = $size.H
|
|
808
|
+
$script:GcrTui.LastFrame = @()
|
|
809
|
+
$script:GcrTui.Dirty = $true
|
|
810
|
+
}
|
|
811
|
+
$guard = 0
|
|
812
|
+
while ($guard -lt 8) {
|
|
813
|
+
$k = Read-GcrTuiKey -TimeoutMs 0
|
|
814
|
+
if ($null -eq $k) { break }
|
|
815
|
+
Invoke-GcrTuiKey -Key $k
|
|
816
|
+
$guard++
|
|
817
|
+
}
|
|
818
|
+
$now = Get-Date
|
|
819
|
+
$ms = 1000
|
|
820
|
+
try { $ms = ($now - $script:GcrTui.LastDraw).TotalMilliseconds } catch { $ms = 1000 }
|
|
821
|
+
$anim = ($script:GcrTui.Phase -in @("fetch", "init", "list", "scan"))
|
|
822
|
+
$need = [bool]$Force -or [bool]$script:GcrTui.Dirty -or ($anim -and $ms -ge 250)
|
|
823
|
+
if ($need -and $ms -ge 50) {
|
|
824
|
+
Render-GcrTui
|
|
825
|
+
$script:GcrTui.Dirty = $false
|
|
826
|
+
$script:GcrTui.LastDraw = $now
|
|
827
|
+
$script:GcrTui.Tick++
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function Wait-GcrTuiPaused {
|
|
832
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
833
|
+
while ($script:GcrTui.Paused -and -not $script:GcrTui.QuitRequested) {
|
|
834
|
+
Invoke-GcrTuiTick
|
|
835
|
+
Start-Sleep -Milliseconds 80
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function Get-GcrDashGuide {
|
|
840
|
+
if ($script:GcrTui.Help) { return "快捷键说明。任意键关闭此帮助。" }
|
|
841
|
+
if ($script:GcrTui.FailView) { return "失败文件列表。F 返回活动日志,再次运行同一命令会重试。" }
|
|
842
|
+
if ($script:GcrTui.Screen -eq "result") { return "本次运行已结束。Enter 关闭界面,进度保留在 .git/partial-resume/。" }
|
|
843
|
+
if ($script:GcrTui.QuitRequested) { return "将在当前 git 命令结束后停止。Ctrl+C 再按一次立即结束。" }
|
|
844
|
+
if ($script:GcrTui.Paused) { return "已暂停:当前批次结束后停住。Space 继续,Q 停止。" }
|
|
845
|
+
switch ($script:GcrTui.Phase) {
|
|
846
|
+
"init" { return "初始化本地仓库并配置 partial clone(只拉元数据,不拉文件内容)。" }
|
|
847
|
+
"fetch" { return "正在拉取 commit/tree 元数据(blob:none)。文件内容会在下一步按批下载。" }
|
|
848
|
+
"list" { return "枚举仓库文件树。不会为了拿大小去拉全部 blob。" }
|
|
849
|
+
"scan" { return "扫描工作区,跳过已经落盘的文件,其余进入待下载队列。" }
|
|
850
|
+
"download" { return "按批 checkout 文件。中断后重跑同一命令即可续传。" }
|
|
851
|
+
"repair" { return "修复 Windows 上可能被弄乱的 git index。" }
|
|
852
|
+
"done" { return "全部完成。工作区已可用。" }
|
|
853
|
+
"error" { return "出错或未完成。重新运行同一命令即可从断点继续。" }
|
|
854
|
+
default { return "断点续传克隆。Q 停止 · P 暂停 · ? 帮助" }
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function Get-GcrWizardGuide {
|
|
859
|
+
param($St)
|
|
860
|
+
if ($St.ConfirmQuit) { return "退出向导?未开始的克隆不会写入进度。Enter 确定,Esc 取消。" }
|
|
861
|
+
if ($St.Edit) { return "正在编辑。Enter 确认,Esc 取消,Ctrl+V 粘贴。光标用 ← → Home End。" }
|
|
862
|
+
if ($St.Focus -eq "recent") { return "最近任务。Enter 填入 URL/目录/分支,可直接续传未完成的克隆。" }
|
|
863
|
+
$items = Get-GcrWizardItems -St $St
|
|
864
|
+
$sel = [int]$St.Sel
|
|
865
|
+
if ($sel -ge 0 -and $sel -lt $items.Count) {
|
|
866
|
+
$g = [string]$items[$sel].Guide
|
|
867
|
+
if ($g) { return $g }
|
|
868
|
+
}
|
|
869
|
+
return "↑↓ 选择选项,Enter 编辑或开始。每个选项的说明会显示在这一行。"
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function Render-GcrTui {
|
|
873
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
874
|
+
if ($script:GcrTui.Screen -eq "wizard") { return }
|
|
875
|
+
$size = Get-GcrTuiSize
|
|
876
|
+
$w = $size.DrawW
|
|
877
|
+
$h = $size.H
|
|
878
|
+
$script:GcrTui.Width = $size.W
|
|
879
|
+
$script:GcrTui.Height = $h
|
|
880
|
+
$c = @{
|
|
881
|
+
R = Get-GcrColor "reset"
|
|
882
|
+
B = Get-GcrColor "bold"
|
|
883
|
+
D = Get-GcrColor "dim"
|
|
884
|
+
C = Get-GcrColor "cyan"
|
|
885
|
+
G = Get-GcrColor "green"
|
|
886
|
+
Y = Get-GcrColor "yellow"
|
|
887
|
+
E = Get-GcrColor "red"
|
|
888
|
+
W = Get-GcrColor "white"
|
|
889
|
+
T = Get-GcrColor "teal"
|
|
890
|
+
}
|
|
891
|
+
$box = $script:GcrTui.Box
|
|
892
|
+
$lines = New-Object System.Collections.Generic.List[string]
|
|
893
|
+
$inner = [Math]::Max(10, $w - 2)
|
|
894
|
+
function Push-GcrBorder {
|
|
895
|
+
param([string]$Kind)
|
|
896
|
+
$ch = $box.H
|
|
897
|
+
if ($Kind -eq "top") { $plain = $box.TL + ($ch * $inner) + $box.TR }
|
|
898
|
+
elseif ($Kind -eq "bot") { $plain = $box.BL + ($ch * $inner) + $box.BR }
|
|
899
|
+
else { $plain = $box.L + ($ch * $inner) + $box.R }
|
|
900
|
+
[void]$lines.Add($c.D + (Format-GcrCell $plain $w) + $c.R)
|
|
901
|
+
}
|
|
902
|
+
function Push-GcrRow {
|
|
903
|
+
param([string]$Left, [string]$Right = "", [string]$Color = "")
|
|
904
|
+
if (-not $Color) { $Color = $c.W }
|
|
905
|
+
$leftW = Get-GcrDisplayWidth $Left
|
|
906
|
+
$rightW = Get-GcrDisplayWidth $Right
|
|
907
|
+
$gap = $inner - $leftW - $rightW
|
|
908
|
+
if ($gap -lt 1) {
|
|
909
|
+
$keep = [Math]::Max(8, $inner - $rightW - 1)
|
|
910
|
+
$Left = Truncate-GcrDisplay $Left $keep
|
|
911
|
+
$leftW = Get-GcrDisplayWidth $Left
|
|
912
|
+
$gap = $inner - $leftW - $rightW
|
|
913
|
+
if ($gap -lt 0) { $Right = ""; $rightW = 0; $gap = $inner - $leftW }
|
|
914
|
+
if ($gap -lt 0) { $gap = 0 }
|
|
915
|
+
}
|
|
916
|
+
$body = $Left + (" " * $gap) + $Right
|
|
917
|
+
$plainSides = $box.V
|
|
918
|
+
$row = $c.D + $plainSides + $c.R + $Color + $body + $c.R + $c.D + $plainSides + $c.R
|
|
919
|
+
[void]$lines.Add($row)
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
$badge = "RUN"
|
|
923
|
+
$badgeC = $c.C
|
|
924
|
+
if ($script:GcrTui.Resume) { $badge = "RESUME" }
|
|
925
|
+
if ($script:GcrTui.Paused) { $badge = "PAUSED"; $badgeC = $c.Y }
|
|
926
|
+
if ($script:GcrTui.QuitRequested) { $badge = "STOPPING"; $badgeC = $c.Y }
|
|
927
|
+
if ($script:GcrTui.Status -eq "done") { $badge = "DONE"; $badgeC = $c.G }
|
|
928
|
+
if ($script:GcrTui.Status -eq "error") { $badge = "ERROR"; $badgeC = $c.E }
|
|
929
|
+
if ($script:GcrTui.ForceQuit) { $badge = "KILLED"; $badgeC = $c.E }
|
|
930
|
+
|
|
931
|
+
Push-GcrBorder "top"
|
|
932
|
+
$title = " git-clone-resume"
|
|
933
|
+
$sub = $badge + " "
|
|
934
|
+
Push-GcrRow -Left ($title) -Right $sub -Color ($c.B + $c.C)
|
|
935
|
+
Push-GcrBorder "mid"
|
|
936
|
+
|
|
937
|
+
$compact = ($h -lt 16)
|
|
938
|
+
if (-not $compact) {
|
|
939
|
+
Push-GcrRow -Left (" 仓库 " + $(if ($script:GcrTui.RepoUrl) { $script:GcrTui.RepoUrl } else { "-" })) -Color $c.W
|
|
940
|
+
Push-GcrRow -Left (" 目录 " + $(if ($script:GcrTui.OutDir) { $script:GcrTui.OutDir } else { "-" })) -Color $c.D
|
|
941
|
+
$sha = $script:GcrTui.Commit
|
|
942
|
+
if ($sha.Length -gt 12) { $sha = $sha.Substring(0, 12) }
|
|
943
|
+
$refLine = " 引用 " + $(if ($script:GcrTui.Ref) { $script:GcrTui.Ref } else { "HEAD" })
|
|
944
|
+
if ($sha) { $refLine = $refLine + " commit " + $sha }
|
|
945
|
+
Push-GcrRow -Left $refLine -Color $c.D
|
|
946
|
+
$phase = Format-GcrPhaseLabel $script:GcrTui.Phase
|
|
947
|
+
$detail = $script:GcrTui.PhaseDetail
|
|
948
|
+
Push-GcrRow -Left (" 阶段 " + $phase) -Right $detail -Color $c.C
|
|
949
|
+
Push-GcrBorder "mid"
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
$ratio = 0.0
|
|
953
|
+
$pctText = "0%"
|
|
954
|
+
$indet = $false
|
|
955
|
+
if ($script:GcrTui.Phase -eq "fetch" -and $script:GcrTui.GitPercent -ge 0) {
|
|
956
|
+
$ratio = $script:GcrTui.GitPercent / 100.0
|
|
957
|
+
$pctText = ("{0}%" -f $script:GcrTui.GitPercent)
|
|
958
|
+
} elseif ($script:GcrTui.Total -gt 0) {
|
|
959
|
+
$ratio = $script:GcrTui.Ok / [double]$script:GcrTui.Total
|
|
960
|
+
$pctText = ("{0:N1}%" -f (100.0 * $ratio))
|
|
961
|
+
} elseif ($script:GcrTui.Phase -in @("fetch", "init", "list")) {
|
|
962
|
+
$indet = $true
|
|
963
|
+
$pctText = "..."
|
|
964
|
+
}
|
|
965
|
+
$barW = [Math]::Max(10, $inner - 12)
|
|
966
|
+
$bar = New-GcrBar -Width $barW -Ratio $ratio -Indeterminate:$indet -Tick $script:GcrTui.Tick
|
|
967
|
+
$barColor = $c.G
|
|
968
|
+
if ($script:GcrTui.Fail -gt 0) { $barColor = $c.Y }
|
|
969
|
+
if ($script:GcrTui.Status -eq "error") { $barColor = $c.E }
|
|
970
|
+
Push-GcrRow -Left (" " + $bar) -Right $pctText -Color $barColor
|
|
971
|
+
|
|
972
|
+
$bytes = ""
|
|
973
|
+
try { $bytes = Format-Bytes ([int64]$script:GcrTui.Bytes) } catch { $bytes = [string]$script:GcrTui.Bytes }
|
|
974
|
+
$stats = (" {0}/{1} 失败 {2} {3} {4:N1}/s ETA {5}" -f @(
|
|
975
|
+
$script:GcrTui.Ok,
|
|
976
|
+
$script:GcrTui.Total,
|
|
977
|
+
$script:GcrTui.Fail,
|
|
978
|
+
$bytes,
|
|
979
|
+
$script:GcrTui.Rate,
|
|
980
|
+
$script:GcrTui.Eta
|
|
981
|
+
))
|
|
982
|
+
Push-GcrRow -Left $stats -Color $c.W
|
|
983
|
+
if (-not $compact) {
|
|
984
|
+
$cur = $script:GcrTui.CurrentFile
|
|
985
|
+
if (-not $cur) { $cur = "-" }
|
|
986
|
+
Push-GcrRow -Left (" 当前 " + $cur) -Color $c.D
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
$footerReserve = 4
|
|
990
|
+
$used = $lines.Count
|
|
991
|
+
$activityH = $h - $used - $footerReserve
|
|
992
|
+
if ($activityH -lt 0) { $activityH = 0 }
|
|
993
|
+
|
|
994
|
+
if ($activityH -gt 0) {
|
|
995
|
+
Push-GcrBorder "mid"
|
|
996
|
+
$activityH = $h - $lines.Count - $footerReserve
|
|
997
|
+
if ($activityH -lt 1) { $activityH = 1 }
|
|
998
|
+
|
|
999
|
+
$bodyLines = New-Object System.Collections.ArrayList
|
|
1000
|
+
if ($script:GcrTui.Screen -eq "result" -and @($script:GcrTui.ResultBody).Count -gt 0) {
|
|
1001
|
+
foreach ($b in @($script:GcrTui.ResultBody)) {
|
|
1002
|
+
[void]$bodyLines.Add(@{ L = "INFO"; M = [string]$b; T = $null })
|
|
1003
|
+
}
|
|
1004
|
+
} elseif ($script:GcrTui.Help) {
|
|
1005
|
+
foreach ($b in @(
|
|
1006
|
+
"键盘",
|
|
1007
|
+
" Q / Ctrl+C 停止(当前 git 命令结束后生效;再按一次强制结束)",
|
|
1008
|
+
" P / Esc 当前批次结束后暂停",
|
|
1009
|
+
" Space 从暂停恢复",
|
|
1010
|
+
" F 切换失败文件列表",
|
|
1011
|
+
" Up/Down j k 滚动活动日志",
|
|
1012
|
+
" End 跟随最新日志",
|
|
1013
|
+
" ? / H 打开或关闭本帮助",
|
|
1014
|
+
"",
|
|
1015
|
+
"续传:重新运行同一条命令。进度在 .git/partial-resume/",
|
|
1016
|
+
"脚本模式:加 -NoTui。强制界面:加 -Tui。"
|
|
1017
|
+
)) {
|
|
1018
|
+
[void]$bodyLines.Add(@{ L = "INFO"; M = $b; T = $null })
|
|
1019
|
+
}
|
|
1020
|
+
} elseif ($script:GcrTui.FailView) {
|
|
1021
|
+
if ($script:GcrTui.Failures.Count -eq 0) {
|
|
1022
|
+
[void]$bodyLines.Add(@{ L = "INFO"; M = "暂无失败文件。"; T = $null })
|
|
1023
|
+
} else {
|
|
1024
|
+
foreach ($f in $script:GcrTui.Failures) {
|
|
1025
|
+
[void]$bodyLines.Add(@{ L = "ERROR"; M = $f; T = $null })
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
} else {
|
|
1029
|
+
foreach ($e in $script:GcrTui.Logs) { [void]$bodyLines.Add($e) }
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
$view = @($bodyLines)
|
|
1033
|
+
$n = $view.Count
|
|
1034
|
+
$off = [int]$script:GcrTui.LogOffset
|
|
1035
|
+
$end = $n - $off
|
|
1036
|
+
if ($end -lt 0) { $end = 0 }
|
|
1037
|
+
$start = $end - $activityH
|
|
1038
|
+
if ($start -lt 0) { $start = 0 }
|
|
1039
|
+
for ($row = 0; $row -lt $activityH; $row++) {
|
|
1040
|
+
$idx = $start + $row
|
|
1041
|
+
if ($idx -ge $end) {
|
|
1042
|
+
Push-GcrRow -Left "" -Color $c.D
|
|
1043
|
+
continue
|
|
1044
|
+
}
|
|
1045
|
+
$item = $view[$idx]
|
|
1046
|
+
$prefix = ""
|
|
1047
|
+
if ($item.T) { $prefix = ([datetime]$item.T).ToString("HH:mm:ss") + " " }
|
|
1048
|
+
$lvl = [string]$item.L
|
|
1049
|
+
if (-not $lvl) { $lvl = "INFO" }
|
|
1050
|
+
$text = $prefix + $item.M
|
|
1051
|
+
Push-GcrRow -Left (" " + $text) -Color (Get-GcrLevelColor $lvl)
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
Push-GcrBorder "mid"
|
|
1056
|
+
$guide = Get-GcrDashGuide
|
|
1057
|
+
Push-GcrRow -Left (" " + $guide) -Color $c.C
|
|
1058
|
+
$hint = ""
|
|
1059
|
+
if ($script:GcrTui.Screen -eq "result") {
|
|
1060
|
+
$hint = " Enter 关闭 · Q 退出 · ? 帮助"
|
|
1061
|
+
} elseif ($script:GcrTui.Help) {
|
|
1062
|
+
$hint = " 任意键关闭帮助"
|
|
1063
|
+
} elseif ($script:GcrTui.QuitRequested) {
|
|
1064
|
+
$hint = " Ctrl+C 再按一次强制结束 · ? 帮助"
|
|
1065
|
+
} elseif ($script:GcrTui.Paused) {
|
|
1066
|
+
$hint = " Space 继续 · Q 停止 · ? 帮助"
|
|
1067
|
+
} else {
|
|
1068
|
+
$hint = " Q 停止 · P 暂停 · F 失败 · ? 帮助 · ↑↓ 日志"
|
|
1069
|
+
}
|
|
1070
|
+
$hintColor = $c.D
|
|
1071
|
+
if ($script:GcrTui.Paused -or $script:GcrTui.QuitRequested) { $hintColor = $c.Y }
|
|
1072
|
+
Push-GcrRow -Left $hint -Color $hintColor
|
|
1073
|
+
Push-GcrBorder "bot"
|
|
1074
|
+
|
|
1075
|
+
while ($lines.Count -gt $h) { $lines.RemoveAt($lines.Count - 1) }
|
|
1076
|
+
while ($lines.Count -lt $h) { [void]$lines.Add("") }
|
|
1077
|
+
Out-GcrFrame -Lines $lines.ToArray()
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function Show-GcrTuiResult {
|
|
1081
|
+
param(
|
|
1082
|
+
[string]$Title,
|
|
1083
|
+
[string[]]$Body,
|
|
1084
|
+
[ValidateSet("done", "error")]
|
|
1085
|
+
[string]$Kind = "done"
|
|
1086
|
+
)
|
|
1087
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
1088
|
+
$script:GcrTui.Screen = "result"
|
|
1089
|
+
$script:GcrTui.Status = $Kind
|
|
1090
|
+
$script:GcrTui.Phase = $(if ($Kind -eq "done") { "done" } else { "error" })
|
|
1091
|
+
$script:GcrTui.ResultTitle = $Title
|
|
1092
|
+
$script:GcrTui.ResultBody = @($Body)
|
|
1093
|
+
$script:GcrTui.Help = $false
|
|
1094
|
+
$script:GcrTui.Paused = $false
|
|
1095
|
+
Add-GcrTuiLog -Level $(if ($Kind -eq "done") { "OK" } else { "ERROR" }) -Message $Title
|
|
1096
|
+
$script:GcrTui.Dirty = $true
|
|
1097
|
+
$pct = 100
|
|
1098
|
+
if ($script:GcrTui.Total -gt 0) {
|
|
1099
|
+
$pct = [int][Math]::Round(100.0 * $script:GcrTui.Ok / $script:GcrTui.Total)
|
|
1100
|
+
}
|
|
1101
|
+
$state = 1
|
|
1102
|
+
if ($Kind -eq "error") { $state = 2 }
|
|
1103
|
+
Set-GcrTuiTabProgress -Percent $pct -State $state
|
|
1104
|
+
Render-GcrTui
|
|
1105
|
+
$waited = 0
|
|
1106
|
+
while ($waited -lt 3600000) {
|
|
1107
|
+
$k = Read-GcrTuiKey -TimeoutMs 200
|
|
1108
|
+
if ($null -ne $k) {
|
|
1109
|
+
if ($k.Key -eq "Enter" -or $k.Key -eq "Q" -or $k.Key -eq "Escape" -or (Test-GcrCtrlKey $k "C")) {
|
|
1110
|
+
break
|
|
1111
|
+
}
|
|
1112
|
+
if ($k.KeyChar -eq "?") { $script:GcrTui.Help = -not $script:GcrTui.Help; Render-GcrTui }
|
|
1113
|
+
}
|
|
1114
|
+
$waited += 200
|
|
1115
|
+
$size = Get-GcrTuiSize
|
|
1116
|
+
if ($size.W -ne $script:GcrTui.Width -or $size.H -ne $script:GcrTui.Height) {
|
|
1117
|
+
$script:GcrTui.LastFrame = @()
|
|
1118
|
+
Render-GcrTui
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function Test-GcrRepoUrlText {
|
|
1124
|
+
param([string]$Text)
|
|
1125
|
+
if ([string]::IsNullOrWhiteSpace($Text)) { return $false }
|
|
1126
|
+
$s = $Text.Trim()
|
|
1127
|
+
if ($s -match "\s") { return $false }
|
|
1128
|
+
if ($s -match "^(https?|git|ssh)://") { return $true }
|
|
1129
|
+
if ($s -match "^[\w.-]+@[\w.-]+:") { return $true }
|
|
1130
|
+
if ($s -match "\.git$") { return $true }
|
|
1131
|
+
if ($s -match "^(github\.com|gitlab\.com|gitee\.com|bitbucket\.org)[/:]") { return $true }
|
|
1132
|
+
if ($s -match "^[A-Za-z]:[\\/]") { return $true }
|
|
1133
|
+
if ($s -match "^\\\\") { return $true }
|
|
1134
|
+
return $false
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function Get-GcrFolderNameSafe {
|
|
1138
|
+
param([string]$Url)
|
|
1139
|
+
if (Get-Command Get-RepoFolderName -ErrorAction SilentlyContinue) {
|
|
1140
|
+
return Get-RepoFolderName -Url $Url
|
|
1141
|
+
}
|
|
1142
|
+
$s = $Url.Trim().TrimEnd([char]47, [char]92)
|
|
1143
|
+
if ($s.Length -ge 4 -and $s.EndsWith(".git", [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
1144
|
+
$s = $s.Substring(0, $s.Length - 4)
|
|
1145
|
+
}
|
|
1146
|
+
$s = $s.Replace([char]92, [char]47)
|
|
1147
|
+
$i = $s.LastIndexOf([char]47)
|
|
1148
|
+
if ($i -ge 0) { $s = $s.Substring($i + 1) }
|
|
1149
|
+
if ([string]::IsNullOrWhiteSpace($s)) { return "repo" }
|
|
1150
|
+
return $s
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function Get-GcrClipboardText {
|
|
1154
|
+
try {
|
|
1155
|
+
$t = Get-Clipboard -Raw -ErrorAction Stop
|
|
1156
|
+
if ($null -eq $t) { return "" }
|
|
1157
|
+
return ([string]$t).Trim()
|
|
1158
|
+
} catch { return "" }
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function Convert-GcrWizardResult {
|
|
1162
|
+
param($St)
|
|
1163
|
+
$include = New-Object System.Collections.Generic.List[string]
|
|
1164
|
+
$exclude = New-Object System.Collections.Generic.List[string]
|
|
1165
|
+
$incText = ""
|
|
1166
|
+
$excText = ""
|
|
1167
|
+
try { $incText = [string]$St.Include } catch { }
|
|
1168
|
+
try { $excText = [string]$St.Exclude } catch { }
|
|
1169
|
+
if (-not [string]::IsNullOrWhiteSpace($incText)) {
|
|
1170
|
+
foreach ($p in @($incText -split "[,;]")) {
|
|
1171
|
+
$t = $p.Trim()
|
|
1172
|
+
if ($t) { [void]$include.Add($t) }
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (-not [string]::IsNullOrWhiteSpace($excText)) {
|
|
1176
|
+
foreach ($p in @($excText -split "[,;]")) {
|
|
1177
|
+
$t = $p.Trim()
|
|
1178
|
+
if ($t) { [void]$exclude.Add($t) }
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
$depth = $null
|
|
1182
|
+
$depthText = ""
|
|
1183
|
+
try { $depthText = [string]$St.Depth } catch { }
|
|
1184
|
+
if ($depthText -match "^\d+$") { $depth = [int]$depthText }
|
|
1185
|
+
$url = ""
|
|
1186
|
+
try { $url = ([string]$St.Url).Trim() } catch { }
|
|
1187
|
+
$dir = ""
|
|
1188
|
+
try { $dir = [string]$St.OutDir } catch { }
|
|
1189
|
+
if ([string]::IsNullOrWhiteSpace($dir)) { $dir = Get-GcrFolderNameSafe -Url $url }
|
|
1190
|
+
$ref = "HEAD"
|
|
1191
|
+
try {
|
|
1192
|
+
if ($St.Ref) { $ref = ([string]$St.Ref).Trim() }
|
|
1193
|
+
} catch { }
|
|
1194
|
+
if ([string]::IsNullOrWhiteSpace($ref)) { $ref = "HEAD" }
|
|
1195
|
+
$batch = 32
|
|
1196
|
+
$retries = 8
|
|
1197
|
+
try { if ($St.BatchSize) { $batch = [int]$St.BatchSize } } catch { }
|
|
1198
|
+
try { if ($St.MaxRetries) { $retries = [int]$St.MaxRetries } } catch { }
|
|
1199
|
+
if ($batch -lt 1) { $batch = 32 }
|
|
1200
|
+
if ($retries -lt 1) { $retries = 8 }
|
|
1201
|
+
$obj = New-Object psobject
|
|
1202
|
+
Add-Member -InputObject $obj -NotePropertyName RepoUrl -NotePropertyValue $url
|
|
1203
|
+
Add-Member -InputObject $obj -NotePropertyName OutDir -NotePropertyValue $dir
|
|
1204
|
+
Add-Member -InputObject $obj -NotePropertyName Ref -NotePropertyValue $ref
|
|
1205
|
+
Add-Member -InputObject $obj -NotePropertyName BatchSize -NotePropertyValue $batch
|
|
1206
|
+
Add-Member -InputObject $obj -NotePropertyName MaxRetries -NotePropertyValue $retries
|
|
1207
|
+
Add-Member -InputObject $obj -NotePropertyName Include -NotePropertyValue $include.ToArray()
|
|
1208
|
+
Add-Member -InputObject $obj -NotePropertyName Exclude -NotePropertyValue $exclude.ToArray()
|
|
1209
|
+
Add-Member -InputObject $obj -NotePropertyName Depth -NotePropertyValue $depth
|
|
1210
|
+
Add-Member -InputObject $obj -NotePropertyName Verify -NotePropertyValue ([bool]$St.Verify)
|
|
1211
|
+
Add-Member -InputObject $obj -NotePropertyName ForceRefetch -NotePropertyValue ([bool]$St.ForceRefetch)
|
|
1212
|
+
Add-Member -InputObject $obj -NotePropertyName DryRun -NotePropertyValue ([bool]$St.DryRun)
|
|
1213
|
+
return ,$obj
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function ConvertFrom-GcrWizardOutput {
|
|
1217
|
+
param($Raw)
|
|
1218
|
+
if ($null -eq $Raw) { return $null }
|
|
1219
|
+
foreach ($x in @($Raw)) {
|
|
1220
|
+
if ($null -eq $x) { continue }
|
|
1221
|
+
if ($x -is [hashtable]) {
|
|
1222
|
+
if ($x.ContainsKey("RepoUrl")) { return $x }
|
|
1223
|
+
continue
|
|
1224
|
+
}
|
|
1225
|
+
try {
|
|
1226
|
+
if ($null -ne $x.PSObject.Properties["RepoUrl"]) { return $x }
|
|
1227
|
+
} catch { }
|
|
1228
|
+
}
|
|
1229
|
+
return $null
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function Render-GcrTuiWizard {
|
|
1233
|
+
param($St)
|
|
1234
|
+
$size = Get-GcrTuiSize
|
|
1235
|
+
$w = $size.DrawW
|
|
1236
|
+
$h = $size.H
|
|
1237
|
+
$inner = [Math]::Max(10, $w - 2)
|
|
1238
|
+
$c = @{
|
|
1239
|
+
R = Get-GcrColor "reset"
|
|
1240
|
+
B = Get-GcrColor "bold"
|
|
1241
|
+
D = Get-GcrColor "dim"
|
|
1242
|
+
C = Get-GcrColor "cyan"
|
|
1243
|
+
G = Get-GcrColor "green"
|
|
1244
|
+
Y = Get-GcrColor "yellow"
|
|
1245
|
+
E = Get-GcrColor "red"
|
|
1246
|
+
W = Get-GcrColor "white"
|
|
1247
|
+
}
|
|
1248
|
+
$box = $script:GcrTui.Box
|
|
1249
|
+
$lines = New-Object System.Collections.Generic.List[string]
|
|
1250
|
+
function WBorder([string]$Kind) {
|
|
1251
|
+
$ch = $box.H
|
|
1252
|
+
if ($Kind -eq "top") { $plain = $box.TL + ($ch * $inner) + $box.TR }
|
|
1253
|
+
elseif ($Kind -eq "bot") { $plain = $box.BL + ($ch * $inner) + $box.BR }
|
|
1254
|
+
else { $plain = $box.L + ($ch * $inner) + $box.R }
|
|
1255
|
+
[void]$lines.Add($c.D + (Format-GcrCell $plain $w) + $c.R)
|
|
1256
|
+
}
|
|
1257
|
+
function WRow([string]$Text, [string]$Color, [switch]$Sel) {
|
|
1258
|
+
if (-not $Color) { $Color = $c.W }
|
|
1259
|
+
$body = Format-GcrCell -Text $Text -Width $inner
|
|
1260
|
+
if ($Sel) { $Color = (Get-GcrColor "rev") + $Color }
|
|
1261
|
+
$row = $c.D + $box.V + $c.R + $Color + $body + $c.R + $c.D + $box.V + $c.R
|
|
1262
|
+
[void]$lines.Add($row)
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
$items = Get-GcrWizardItems -St $St
|
|
1266
|
+
WBorder "top"
|
|
1267
|
+
WRow " git-clone-resume" ($c.B + $c.C)
|
|
1268
|
+
WRow " 断点续传克隆 · partial clone + 按批 checkout" $c.D
|
|
1269
|
+
WBorder "mid"
|
|
1270
|
+
|
|
1271
|
+
$formEnd = 11
|
|
1272
|
+
$recent = @($St.Recent)
|
|
1273
|
+
$maxFormVisible = [Math]::Max(6, $h - 10)
|
|
1274
|
+
if ($maxFormVisible -gt ($formEnd + 1)) { $maxFormVisible = $formEnd + 1 }
|
|
1275
|
+
$sel = [int]$St.Sel
|
|
1276
|
+
$top = [int]$St.Scroll
|
|
1277
|
+
if ($sel -le $formEnd) {
|
|
1278
|
+
if ($sel -lt $top) { $top = $sel }
|
|
1279
|
+
if ($sel -ge $top + $maxFormVisible) { $top = $sel - $maxFormVisible + 1 }
|
|
1280
|
+
if ($top -lt 0) { $top = 0 }
|
|
1281
|
+
$St.Scroll = $top
|
|
1282
|
+
}
|
|
1283
|
+
$end = [Math]::Min($formEnd, $top + $maxFormVisible - 1)
|
|
1284
|
+
for ($i = $top; $i -le $end; $i++) {
|
|
1285
|
+
$it = $items[$i]
|
|
1286
|
+
$mark = " "
|
|
1287
|
+
if ($sel -eq $i) { $mark = " " + $script:GcrTui.Box.Pointer }
|
|
1288
|
+
$val = [string]$it.Value
|
|
1289
|
+
if ($it.Kind -eq "bool") { $val = $(if ($it.Flag) { "开" } else { "关" }) }
|
|
1290
|
+
if ($St.Edit -and $sel -eq $i) {
|
|
1291
|
+
$buf = [string]$St.EditBuf
|
|
1292
|
+
$cur = [int]$St.EditCur
|
|
1293
|
+
if ($cur -lt 0) { $cur = 0 }
|
|
1294
|
+
if ($cur -gt $buf.Length) { $cur = $buf.Length }
|
|
1295
|
+
$val = $buf.Insert($cur, "|")
|
|
1296
|
+
}
|
|
1297
|
+
$label = Format-GcrCell $it.Label 14
|
|
1298
|
+
$text = $mark + " " + $label + " " + $val
|
|
1299
|
+
$col = $c.W
|
|
1300
|
+
if ($it.Kind -eq "start") { $col = $c.G + $c.B }
|
|
1301
|
+
WRow $text $col -Sel:($sel -eq $i -and -not $St.Edit)
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
WBorder "mid"
|
|
1305
|
+
WRow " 最近任务 (Tab 切换 · Enter 填入)" $c.D
|
|
1306
|
+
$recentSlots = $h - $lines.Count - 4
|
|
1307
|
+
if ($recentSlots -lt 1) { $recentSlots = 1 }
|
|
1308
|
+
if ($recent.Count -eq 0) {
|
|
1309
|
+
WRow " (无。完成一次克隆后会出现在这里)" $c.D
|
|
1310
|
+
$recentSlots--
|
|
1311
|
+
} else {
|
|
1312
|
+
$show = [Math]::Min($recent.Count, [Math]::Max(1, $recentSlots))
|
|
1313
|
+
for ($r = 0; $r -lt $show; $r++) {
|
|
1314
|
+
$it = $recent[$r]
|
|
1315
|
+
$name = [string]$it.outDir
|
|
1316
|
+
if ($name) { $name = Split-Path -Leaf $name }
|
|
1317
|
+
if (-not $name) { $name = [string]$it.url }
|
|
1318
|
+
$pct = ""
|
|
1319
|
+
try {
|
|
1320
|
+
if ([int]$it.total -gt 0) { $pct = ("{0}%" -f [int](100 * [int]$it.ok / [int]$it.total)) }
|
|
1321
|
+
} catch { }
|
|
1322
|
+
$stt = [string]$it.status
|
|
1323
|
+
$ago = ""
|
|
1324
|
+
try { $ago = Format-GcrAgo $it.updated } catch { }
|
|
1325
|
+
$mark = " "
|
|
1326
|
+
$isSel = ($St.Focus -eq "recent" -and [int]$St.RecentSel -eq $r)
|
|
1327
|
+
if ($isSel) { $mark = " " + $script:GcrTui.Box.Pointer + " " }
|
|
1328
|
+
$text = $mark + $name + " " + $stt + " " + $pct + " " + $ago
|
|
1329
|
+
WRow $text $(if ($isSel) { $c.C } else { $c.D }) -Sel:$isSel
|
|
1330
|
+
$recentSlots--
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
while ($recentSlots -gt 0) { WRow "" $c.D; $recentSlots-- }
|
|
1334
|
+
|
|
1335
|
+
WBorder "mid"
|
|
1336
|
+
$guide = Get-GcrWizardGuide -St $St
|
|
1337
|
+
if ($St.Error) { $guide = [string]$St.Error }
|
|
1338
|
+
$guideColor = $c.C
|
|
1339
|
+
if ($St.Error) { $guideColor = $c.E }
|
|
1340
|
+
if ($St.ConfirmQuit) { $guideColor = $c.Y }
|
|
1341
|
+
WRow (" " + $guide) $guideColor
|
|
1342
|
+
$foot = " Enter 编辑/开始 · Space 开关 · ←→ 改批次 · Ctrl+V 粘贴 · Q 退出"
|
|
1343
|
+
if ($St.Edit) { $foot = " Enter 确认 · Esc 取消 · Ctrl+V 粘贴" }
|
|
1344
|
+
if ($St.ConfirmQuit) { $foot = " Enter 确定退出 · Esc 返回" }
|
|
1345
|
+
WRow $foot $c.D
|
|
1346
|
+
WBorder "bot"
|
|
1347
|
+
while ($lines.Count -gt $h) { $lines.RemoveAt($lines.Count - 1) }
|
|
1348
|
+
while ($lines.Count -lt $h) { [void]$lines.Add("") }
|
|
1349
|
+
Out-GcrFrame -Lines $lines.ToArray()
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function Get-GcrWizardItems {
|
|
1353
|
+
param($St)
|
|
1354
|
+
return @(
|
|
1355
|
+
@{ Id = "url"; Kind = "text"; Label = "仓库 URL"; Value = $St.Url; Flag = $false; Guide = "远程仓库地址,支持 https、ssh、git@ 以及本地路径。Ctrl+V 从剪贴板粘贴。" }
|
|
1356
|
+
@{ Id = "dir"; Kind = "text"; Label = "本地目录"; Value = $(if ($St.OutDir) { $St.OutDir } else { "(自动)" }); Flag = $false; Guide = "工作区目录。留空则用仓库名。已有 .git/partial-resume 时自动续传。" }
|
|
1357
|
+
@{ Id = "ref"; Kind = "text"; Label = "分支/标签"; Value = $St.Ref; Flag = $false; Guide = "分支、标签或 commit SHA。默认远程 HEAD。" }
|
|
1358
|
+
@{ Id = "batch"; Kind = "enum"; Label = "每批文件"; Value = [string]$St.BatchSize; Flag = $false; Guide = "每批 checkout 的文件数。越大越快,中断粒度越粗。← → 调整。" }
|
|
1359
|
+
@{ Id = "retry"; Kind = "enum"; Label = "重试次数"; Value = [string]$St.MaxRetries; Flag = $false; Guide = "单文件失败后的最大重试次数。← → 调整。网络不稳时可调大。" }
|
|
1360
|
+
@{ Id = "include"; Kind = "text"; Label = "只含路径"; Value = $(if ($St.Include) { $St.Include } else { "(全部)" }); Flag = $false; Guide = "只下载匹配的路径,逗号分隔通配符,例如 src/*,docs/*。空表示全部。" }
|
|
1361
|
+
@{ Id = "exclude"; Kind = "text"; Label = "排除路径"; Value = $(if ($St.Exclude) { $St.Exclude } else { "(无)" }); Flag = $false; Guide = "跳过匹配的路径,例如 *.bin,*.zip。可与「只含路径」同时使用。" }
|
|
1362
|
+
@{ Id = "depth"; Kind = "text"; Label = "浅克隆深度"; Value = $(if ($St.Depth) { $St.Depth } else { "(完整历史)" }); Flag = $false; Guide = "浅克隆深度。留空则拉完整 commit 历史(仍然不拉 blob)。" }
|
|
1363
|
+
@{ Id = "verify"; Kind = "bool"; Label = "哈希校验"; Value = ""; Flag = [bool]$St.Verify; Guide = "续传时对已有文件做 hash-object 校验,哈希不一致则重新下载。Space 开关。" }
|
|
1364
|
+
@{ Id = "force"; Kind = "bool"; Label = "强制 refetch"; Value = ""; Flag = [bool]$St.ForceRefetch; Guide = "强制重新 fetch 目标 ref。换分支或更新到最新 commit 时打开。Space 开关。" }
|
|
1365
|
+
@{ Id = "dry"; Kind = "bool"; Label = "DryRun"; Value = ""; Flag = [bool]$St.DryRun; Guide = "只列出将要处理的文件,不下载 blob。适合先看清单。Space 开关。" }
|
|
1366
|
+
@{ Id = "start"; Kind = "start"; Label = "开始克隆"; Value = ""; Flag = $false; Guide = "按上面的设置开始或继续克隆。Enter 启动。中断后重跑即可续传。" }
|
|
1367
|
+
)
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function Apply-GcrRecentToWizard {
|
|
1371
|
+
param($St, $Item)
|
|
1372
|
+
if ($null -eq $Item) { return }
|
|
1373
|
+
if ($Item.url) { $St.Url = [string]$Item.url }
|
|
1374
|
+
if ($Item.outDir) { $St.OutDir = [string]$Item.outDir; $St.OutDirAuto = $false }
|
|
1375
|
+
if ($Item.ref) { $St.Ref = [string]$Item.ref }
|
|
1376
|
+
$St.Focus = "form"
|
|
1377
|
+
$St.Sel = 11
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function Show-GcrTuiWizard {
|
|
1381
|
+
param([hashtable]$Defaults)
|
|
1382
|
+
if (-not (Test-GcrTuiActive)) {
|
|
1383
|
+
if (-not (Initialize-GcrTui)) { return $null }
|
|
1384
|
+
}
|
|
1385
|
+
$script:GcrTui.Screen = "wizard"
|
|
1386
|
+
$script:GcrTui.Phase = "wizard"
|
|
1387
|
+
$url = ""
|
|
1388
|
+
if ($Defaults -and $Defaults.ContainsKey("RepoUrl")) { $url = [string]$Defaults.RepoUrl }
|
|
1389
|
+
if (-not $url) {
|
|
1390
|
+
$clip = Get-GcrClipboardText
|
|
1391
|
+
if (Test-GcrRepoUrlText $clip) { $url = $clip }
|
|
1392
|
+
}
|
|
1393
|
+
$dir = ""
|
|
1394
|
+
$dirAuto = $true
|
|
1395
|
+
if ($Defaults -and $Defaults.ContainsKey("OutDir") -and $Defaults.OutDir) {
|
|
1396
|
+
$dir = [string]$Defaults.OutDir
|
|
1397
|
+
$dirAuto = $false
|
|
1398
|
+
}
|
|
1399
|
+
$ref = "HEAD"
|
|
1400
|
+
if ($Defaults -and $Defaults.ContainsKey("Ref") -and $Defaults.Ref) { $ref = [string]$Defaults.Ref }
|
|
1401
|
+
$batch = 32
|
|
1402
|
+
if ($Defaults -and $Defaults.ContainsKey("BatchSize") -and $Defaults.BatchSize) { $batch = [int]$Defaults.BatchSize }
|
|
1403
|
+
$retries = 8
|
|
1404
|
+
if ($Defaults -and $Defaults.ContainsKey("MaxRetries") -and $Defaults.MaxRetries) { $retries = [int]$Defaults.MaxRetries }
|
|
1405
|
+
$st = @{
|
|
1406
|
+
Url = $url
|
|
1407
|
+
OutDir = $dir
|
|
1408
|
+
OutDirAuto = $dirAuto
|
|
1409
|
+
Ref = $ref
|
|
1410
|
+
BatchSize = $batch
|
|
1411
|
+
MaxRetries = $retries
|
|
1412
|
+
Include = ""
|
|
1413
|
+
Exclude = ""
|
|
1414
|
+
Depth = ""
|
|
1415
|
+
Verify = $false
|
|
1416
|
+
ForceRefetch = $false
|
|
1417
|
+
DryRun = $false
|
|
1418
|
+
Sel = 0
|
|
1419
|
+
RecentSel = 0
|
|
1420
|
+
Focus = "form"
|
|
1421
|
+
Edit = $false
|
|
1422
|
+
EditBuf = ""
|
|
1423
|
+
EditCur = 0
|
|
1424
|
+
EditField = ""
|
|
1425
|
+
Recent = @(Get-GcrHistory)
|
|
1426
|
+
Scroll = 0
|
|
1427
|
+
ConfirmQuit = $false
|
|
1428
|
+
Error = ""
|
|
1429
|
+
Help = $false
|
|
1430
|
+
}
|
|
1431
|
+
if ($Defaults -and $Defaults.ContainsKey("Verify")) { $st.Verify = [bool]$Defaults.Verify }
|
|
1432
|
+
if ($Defaults -and $Defaults.ContainsKey("ForceRefetch")) { $st.ForceRefetch = [bool]$Defaults.ForceRefetch }
|
|
1433
|
+
if ($Defaults -and $Defaults.ContainsKey("DryRun")) { $st.DryRun = [bool]$Defaults.DryRun }
|
|
1434
|
+
if ($Defaults -and $Defaults.ContainsKey("Include") -and $Defaults.Include) {
|
|
1435
|
+
$st.Include = (@($Defaults.Include) -join ",")
|
|
1436
|
+
}
|
|
1437
|
+
if ($Defaults -and $Defaults.ContainsKey("Exclude") -and $Defaults.Exclude) {
|
|
1438
|
+
$st.Exclude = (@($Defaults.Exclude) -join ",")
|
|
1439
|
+
}
|
|
1440
|
+
if ($Defaults -and $Defaults.ContainsKey("Depth") -and $Defaults.Depth) {
|
|
1441
|
+
$st.Depth = [string]$Defaults.Depth
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
$batches = @(8, 16, 32, 64, 128, 256)
|
|
1445
|
+
$retriesSet = @(3, 5, 8, 12, 20)
|
|
1446
|
+
$dirty = $true
|
|
1447
|
+
|
|
1448
|
+
while ($true) {
|
|
1449
|
+
if ($st.OutDirAuto -and $st.Url) {
|
|
1450
|
+
$st.OutDir = Get-GcrFolderNameSafe -Url $st.Url
|
|
1451
|
+
}
|
|
1452
|
+
$size = Get-GcrTuiSize
|
|
1453
|
+
if ($size.W -ne $script:GcrTui.Width -or $size.H -ne $script:GcrTui.Height) {
|
|
1454
|
+
$script:GcrTui.Width = $size.W
|
|
1455
|
+
$script:GcrTui.Height = $size.H
|
|
1456
|
+
$script:GcrTui.LastFrame = @()
|
|
1457
|
+
$dirty = $true
|
|
1458
|
+
}
|
|
1459
|
+
if ($dirty) {
|
|
1460
|
+
Render-GcrTuiWizard -St $st
|
|
1461
|
+
$dirty = $false
|
|
1462
|
+
}
|
|
1463
|
+
$k = Read-GcrTuiKey -TimeoutMs 250
|
|
1464
|
+
if ($null -eq $k) { continue }
|
|
1465
|
+
$dirty = $true
|
|
1466
|
+
|
|
1467
|
+
if ($st.ConfirmQuit) {
|
|
1468
|
+
if ($k.Key -eq "Enter" -or $k.Key -eq "Y" -or $k.Key -eq "Q") { return $null }
|
|
1469
|
+
$st.ConfirmQuit = $false
|
|
1470
|
+
continue
|
|
1471
|
+
}
|
|
1472
|
+
if ($st.Edit) {
|
|
1473
|
+
$buf = [string]$st.EditBuf
|
|
1474
|
+
$cur = [int]$st.EditCur
|
|
1475
|
+
if ($cur -lt 0) { $cur = 0 }
|
|
1476
|
+
if ($cur -gt $buf.Length) { $cur = $buf.Length }
|
|
1477
|
+
if ($k.Key -eq "Escape") { $st.Edit = $false; continue }
|
|
1478
|
+
if ($k.Key -eq "Enter") {
|
|
1479
|
+
$val = $buf
|
|
1480
|
+
switch ($st.EditField) {
|
|
1481
|
+
"url" { $st.Url = $val.Trim() }
|
|
1482
|
+
"dir" {
|
|
1483
|
+
$st.OutDir = $val.Trim()
|
|
1484
|
+
$st.OutDirAuto = [string]::IsNullOrWhiteSpace($st.OutDir)
|
|
1485
|
+
}
|
|
1486
|
+
"ref" { $st.Ref = $(if ($val.Trim()) { $val.Trim() } else { "HEAD" }) }
|
|
1487
|
+
"include" { $st.Include = $val.Trim() }
|
|
1488
|
+
"exclude" { $st.Exclude = $val.Trim() }
|
|
1489
|
+
"depth" { $st.Depth = $val.Trim() }
|
|
1490
|
+
}
|
|
1491
|
+
$st.Edit = $false
|
|
1492
|
+
continue
|
|
1493
|
+
}
|
|
1494
|
+
if (Test-GcrCtrlKey $k "V") {
|
|
1495
|
+
$paste = Get-GcrClipboardText
|
|
1496
|
+
if ($paste) {
|
|
1497
|
+
$paste = ($paste -split "[\r\n]")[0]
|
|
1498
|
+
$buf = $buf.Substring(0, $cur) + $paste + $buf.Substring($cur)
|
|
1499
|
+
$cur = $cur + $paste.Length
|
|
1500
|
+
}
|
|
1501
|
+
} elseif ($k.Key -eq "LeftArrow") {
|
|
1502
|
+
if ($cur -gt 0) { $cur-- }
|
|
1503
|
+
} elseif ($k.Key -eq "RightArrow") {
|
|
1504
|
+
if ($cur -lt $buf.Length) { $cur++ }
|
|
1505
|
+
} elseif ($k.Key -eq "Home") { $cur = 0 }
|
|
1506
|
+
elseif ($k.Key -eq "End") { $cur = $buf.Length }
|
|
1507
|
+
elseif ($k.Key -eq "Backspace") {
|
|
1508
|
+
if ($cur -gt 0) { $buf = $buf.Remove($cur - 1, 1); $cur-- }
|
|
1509
|
+
} elseif ($k.Key -eq "Delete") {
|
|
1510
|
+
if ($cur -lt $buf.Length) { $buf = $buf.Remove($cur, 1) }
|
|
1511
|
+
} elseif (-not [string]::IsNullOrEmpty([string]$k.KeyChar) -and [int][char]$k.KeyChar -ge 32) {
|
|
1512
|
+
$buf = $buf.Insert($cur, [string]$k.KeyChar)
|
|
1513
|
+
$cur++
|
|
1514
|
+
}
|
|
1515
|
+
$st.EditBuf = $buf
|
|
1516
|
+
$st.EditCur = $cur
|
|
1517
|
+
continue
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (Test-GcrCtrlKey $k "C") { $st.ConfirmQuit = $true; continue }
|
|
1521
|
+
if (Test-GcrCtrlKey $k "V" -and $st.Focus -eq "form") {
|
|
1522
|
+
$paste = Get-GcrClipboardText
|
|
1523
|
+
if (Test-GcrRepoUrlText $paste) {
|
|
1524
|
+
$st.Url = $paste
|
|
1525
|
+
if ($st.OutDirAuto) { $st.OutDir = Get-GcrFolderNameSafe -Url $paste }
|
|
1526
|
+
}
|
|
1527
|
+
continue
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
switch ($k.Key.ToString()) {
|
|
1531
|
+
"Q" { $st.ConfirmQuit = $true }
|
|
1532
|
+
"Escape" { $st.ConfirmQuit = $true }
|
|
1533
|
+
"Tab" {
|
|
1534
|
+
if ($st.Focus -eq "form") { $st.Focus = "recent"; if ($st.Recent.Count -eq 0) { $st.Focus = "form" } }
|
|
1535
|
+
else { $st.Focus = "form" }
|
|
1536
|
+
}
|
|
1537
|
+
"UpArrow" {
|
|
1538
|
+
if ($st.Focus -eq "recent") {
|
|
1539
|
+
if ($st.RecentSel -gt 0) { $st.RecentSel-- }
|
|
1540
|
+
else { $st.Focus = "form"; $st.Sel = 11 }
|
|
1541
|
+
} else {
|
|
1542
|
+
if ($st.Sel -gt 0) { $st.Sel-- }
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
"DownArrow" {
|
|
1546
|
+
if ($st.Focus -eq "recent") {
|
|
1547
|
+
if ($st.RecentSel -lt ($st.Recent.Count - 1)) { $st.RecentSel++ }
|
|
1548
|
+
} else {
|
|
1549
|
+
if ($st.Sel -lt 11) { $st.Sel++ }
|
|
1550
|
+
elseif ($st.Recent.Count -gt 0) { $st.Focus = "recent"; $st.RecentSel = 0 }
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
"K" {
|
|
1554
|
+
if ($k.KeyChar -eq "k") {
|
|
1555
|
+
if ($st.Focus -eq "form" -and $st.Sel -gt 0) { $st.Sel-- }
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
"J" {
|
|
1559
|
+
if ($k.KeyChar -eq "j") {
|
|
1560
|
+
if ($st.Focus -eq "form" -and $st.Sel -lt 11) { $st.Sel++ }
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
"LeftArrow" {
|
|
1564
|
+
if ($st.Focus -eq "form" -and $st.Sel -eq 3) {
|
|
1565
|
+
$idx = [array]::IndexOf($batches, [int]$st.BatchSize)
|
|
1566
|
+
if ($idx -lt 0) { $idx = 2 }
|
|
1567
|
+
if ($idx -gt 0) { $st.BatchSize = $batches[$idx - 1] }
|
|
1568
|
+
}
|
|
1569
|
+
if ($st.Focus -eq "form" -and $st.Sel -eq 4) {
|
|
1570
|
+
$idx = [array]::IndexOf($retriesSet, [int]$st.MaxRetries)
|
|
1571
|
+
if ($idx -lt 0) { $idx = 2 }
|
|
1572
|
+
if ($idx -gt 0) { $st.MaxRetries = $retriesSet[$idx - 1] }
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
"RightArrow" {
|
|
1576
|
+
if ($st.Focus -eq "form" -and $st.Sel -eq 3) {
|
|
1577
|
+
$idx = [array]::IndexOf($batches, [int]$st.BatchSize)
|
|
1578
|
+
if ($idx -lt 0) { $idx = 2 }
|
|
1579
|
+
if ($idx -lt $batches.Count - 1) { $st.BatchSize = $batches[$idx + 1] }
|
|
1580
|
+
}
|
|
1581
|
+
if ($st.Focus -eq "form" -and $st.Sel -eq 4) {
|
|
1582
|
+
$idx = [array]::IndexOf($retriesSet, [int]$st.MaxRetries)
|
|
1583
|
+
if ($idx -lt 0) { $idx = 2 }
|
|
1584
|
+
if ($idx -lt $retriesSet.Count - 1) { $st.MaxRetries = $retriesSet[$idx + 1] }
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
"Spacebar" {
|
|
1588
|
+
if ($st.Focus -eq "form") {
|
|
1589
|
+
if ($st.Sel -eq 8) { $st.Verify = -not $st.Verify }
|
|
1590
|
+
elseif ($st.Sel -eq 9) { $st.ForceRefetch = -not $st.ForceRefetch }
|
|
1591
|
+
elseif ($st.Sel -eq 10) { $st.DryRun = -not $st.DryRun }
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
"Enter" {
|
|
1595
|
+
if ($st.Focus -eq "recent") {
|
|
1596
|
+
if ($st.Recent.Count -gt 0) { Apply-GcrRecentToWizard -St $st -Item $st.Recent[$st.RecentSel] }
|
|
1597
|
+
break
|
|
1598
|
+
}
|
|
1599
|
+
$wizItems = @(Get-GcrWizardItems -St $st)
|
|
1600
|
+
if ($st.Sel -lt 0 -or $st.Sel -ge $wizItems.Count) { break }
|
|
1601
|
+
$id = [string]$wizItems[$st.Sel].Id
|
|
1602
|
+
if ($id -eq "start") {
|
|
1603
|
+
if ([string]::IsNullOrWhiteSpace($st.Url) -or $st.Url -match "\s") {
|
|
1604
|
+
$st.Error = "请填写仓库 URL(Ctrl+V 可从剪贴板粘贴)"
|
|
1605
|
+
$st.Sel = 0
|
|
1606
|
+
} else {
|
|
1607
|
+
try {
|
|
1608
|
+
$result = Convert-GcrWizardResult -St $st
|
|
1609
|
+
$script:GcrTui.Screen = "dash"
|
|
1610
|
+
$script:GcrTui.Logs.Clear()
|
|
1611
|
+
return ,$result
|
|
1612
|
+
} catch {
|
|
1613
|
+
$st.Error = "无法开始: " + $_.Exception.Message
|
|
1614
|
+
$script:GcrTui.Screen = "wizard"
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
} elseif ($id -eq "verify") { $st.Verify = -not $st.Verify }
|
|
1618
|
+
elseif ($id -eq "force") { $st.ForceRefetch = -not $st.ForceRefetch }
|
|
1619
|
+
elseif ($id -eq "dry") { $st.DryRun = -not $st.DryRun }
|
|
1620
|
+
elseif ($id -eq "batch" -or $id -eq "retry") { }
|
|
1621
|
+
else {
|
|
1622
|
+
$st.Edit = $true
|
|
1623
|
+
$st.EditField = $id
|
|
1624
|
+
$map = @{
|
|
1625
|
+
url = $st.Url
|
|
1626
|
+
dir = $st.OutDir
|
|
1627
|
+
ref = $st.Ref
|
|
1628
|
+
include = $st.Include
|
|
1629
|
+
exclude = $st.Exclude
|
|
1630
|
+
depth = $st.Depth
|
|
1631
|
+
}
|
|
1632
|
+
$st.EditBuf = [string]$map[$id]
|
|
1633
|
+
$st.EditCur = $st.EditBuf.Length
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
"S" {
|
|
1637
|
+
if (-not [string]::IsNullOrWhiteSpace($st.Url)) {
|
|
1638
|
+
try {
|
|
1639
|
+
$result = Convert-GcrWizardResult -St $st
|
|
1640
|
+
$script:GcrTui.Screen = "dash"
|
|
1641
|
+
$script:GcrTui.Logs.Clear()
|
|
1642
|
+
return ,$result
|
|
1643
|
+
} catch {
|
|
1644
|
+
$st.Error = "无法开始: " + $_.Exception.Message
|
|
1645
|
+
$script:GcrTui.Screen = "wizard"
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
default {
|
|
1650
|
+
if ($k.KeyChar -eq "?") { $st.Error = "Enter 编辑 · Space 开关 · Tab 最近任务 · S 开始 · Q 退出" }
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
if ($k.Key -ne "Enter") { $st.Error = "" }
|
|
1654
|
+
}
|
|
1655
|
+
return $null
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function Show-GcrCliWizard {
|
|
1659
|
+
param([hashtable]$Defaults)
|
|
1660
|
+
Write-Host ""
|
|
1661
|
+
Write-Host "git-clone-resume 交互设置" -ForegroundColor Cyan
|
|
1662
|
+
Write-Host "直接回车使用括号里的默认值。空 URL 则退出。" -ForegroundColor DarkGray
|
|
1663
|
+
Write-Host ""
|
|
1664
|
+
$pre = ""
|
|
1665
|
+
if ($Defaults -and $Defaults.ContainsKey("RepoUrl")) { $pre = [string]$Defaults.RepoUrl }
|
|
1666
|
+
if (-not $pre) {
|
|
1667
|
+
$clip = Get-GcrClipboardText
|
|
1668
|
+
if (Test-GcrRepoUrlText $clip) { $pre = $clip }
|
|
1669
|
+
}
|
|
1670
|
+
$prompt = "仓库 URL"
|
|
1671
|
+
if ($pre) { $prompt = "仓库 URL [$pre]" }
|
|
1672
|
+
$url = Read-Host $prompt
|
|
1673
|
+
if ([string]::IsNullOrWhiteSpace($url)) { $url = $pre }
|
|
1674
|
+
if ([string]::IsNullOrWhiteSpace($url)) { return $null }
|
|
1675
|
+
$defDir = Get-GcrFolderNameSafe -Url $url
|
|
1676
|
+
if ($Defaults -and $Defaults.ContainsKey("OutDir") -and $Defaults.OutDir) { $defDir = [string]$Defaults.OutDir }
|
|
1677
|
+
$dir = Read-Host "本地目录 [$defDir]"
|
|
1678
|
+
if ([string]::IsNullOrWhiteSpace($dir)) { $dir = $defDir }
|
|
1679
|
+
$defRef = "HEAD"
|
|
1680
|
+
if ($Defaults -and $Defaults.ContainsKey("Ref") -and $Defaults.Ref) { $defRef = [string]$Defaults.Ref }
|
|
1681
|
+
$ref = Read-Host "分支/标签/commit [$defRef]"
|
|
1682
|
+
if ([string]::IsNullOrWhiteSpace($ref)) { $ref = $defRef }
|
|
1683
|
+
$batch = 32
|
|
1684
|
+
if ($Defaults -and $Defaults.ContainsKey("BatchSize") -and $Defaults.BatchSize) { $batch = [int]$Defaults.BatchSize }
|
|
1685
|
+
$batchText = Read-Host "每批文件数 [$batch]"
|
|
1686
|
+
if ($batchText -match "^\d+$") { $batch = [int]$batchText }
|
|
1687
|
+
return @{
|
|
1688
|
+
RepoUrl = $url.Trim()
|
|
1689
|
+
OutDir = $dir
|
|
1690
|
+
Ref = $ref
|
|
1691
|
+
BatchSize = $batch
|
|
1692
|
+
MaxRetries = 8
|
|
1693
|
+
Include = @()
|
|
1694
|
+
Exclude = @()
|
|
1695
|
+
Depth = $null
|
|
1696
|
+
Verify = $false
|
|
1697
|
+
ForceRefetch = $false
|
|
1698
|
+
DryRun = $false
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function Show-GcrInteractiveSetup {
|
|
1703
|
+
param([hashtable]$Defaults)
|
|
1704
|
+
$raw = $null
|
|
1705
|
+
if (Test-GcrTuiAvailable) {
|
|
1706
|
+
if (Initialize-GcrTui) {
|
|
1707
|
+
$raw = Show-GcrTuiWizard -Defaults $Defaults
|
|
1708
|
+
return ,(ConvertFrom-GcrWizardOutput $raw)
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
$raw = Show-GcrCliWizard -Defaults $Defaults
|
|
1712
|
+
return ,(ConvertFrom-GcrWizardOutput $raw)
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function Write-GcrNewline {
|
|
1716
|
+
if (Test-GcrTuiActive) { return }
|
|
1717
|
+
Write-Host ""
|
|
1718
|
+
}
|