git-clone-resume 0.1.3 → 0.1.4
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/git-clone-resume.ps1 +1685 -1480
- package/git-clone-resume.tui.ps1 +90 -13
- package/package.json +1 -1
package/git-clone-resume.ps1
CHANGED
|
@@ -1,1480 +1,1685 @@
|
|
|
1
|
-
#Requires -Version 5.1
|
|
2
|
-
<#
|
|
3
|
-
.SYNOPSIS
|
|
4
|
-
Git 仓库 Windows 断点续传克隆(partial clone + 按批 checkout)。
|
|
5
|
-
|
|
6
|
-
.DESCRIPTION
|
|
7
|
-
针对 GitHub 等不稳定网络:先只拉 commit/tree 元数据(--filter=blob:none),
|
|
8
|
-
再分批把工作区文件 checkout 下来。中断后用同一命令重跑即可续传。
|
|
9
|
-
|
|
10
|
-
进度保存在仓库 .git/partial-resume/ 下,不污染工作区。
|
|
11
|
-
已成功落盘且大小(可选哈希)匹配的文件会自动跳过。
|
|
12
|
-
|
|
13
|
-
.PARAMETER RepoUrl
|
|
14
|
-
仓库地址。支持 https / ssh / git@ 以及本地路径。
|
|
15
|
-
|
|
16
|
-
.PARAMETER OutDir
|
|
17
|
-
本地目录。默认取 URL 最后一段(去掉 .git)。
|
|
18
|
-
|
|
19
|
-
.PARAMETER Ref
|
|
20
|
-
分支、标签或 commit。默认远程 HEAD。
|
|
21
|
-
|
|
22
|
-
.PARAMETER BatchSize
|
|
23
|
-
每批最多 checkout 的文件数。越大越快,中断粒度越粗。默认 32。
|
|
24
|
-
|
|
25
|
-
.PARAMETER MaxArgChars
|
|
26
|
-
单次 git 命令行参数最大字符数,避免超过 Windows 限制。默认 6000。
|
|
27
|
-
|
|
28
|
-
.PARAMETER MaxRetries
|
|
29
|
-
单个批次/文件失败后的最大重试次数。默认 8。
|
|
30
|
-
|
|
31
|
-
.PARAMETER RetryDelaySeconds
|
|
32
|
-
首次重试等待秒数,之后指数退避(封顶 60 秒)。默认 2。
|
|
33
|
-
|
|
34
|
-
.PARAMETER Include
|
|
35
|
-
只下载匹配这些通配符的路径(相对仓库根,支持 * 和 ?)。可重复。
|
|
36
|
-
|
|
37
|
-
.PARAMETER Exclude
|
|
38
|
-
排除匹配这些通配符的路径。可重复。
|
|
39
|
-
|
|
40
|
-
.PARAMETER Depth
|
|
41
|
-
可选浅克隆深度。不指定则拉完整 commit 历史(仍不拉 blob)。
|
|
42
|
-
|
|
43
|
-
.PARAMETER Verify
|
|
44
|
-
续传时对已存在文件做 hash-object 校验,哈希不一致则重下。
|
|
45
|
-
|
|
46
|
-
.PARAMETER ForceRefetch
|
|
47
|
-
强制重新 fetch 目标 ref(默认仅在本地还没有该 commit 时 fetch)。
|
|
48
|
-
|
|
49
|
-
.PARAMETER DryRun
|
|
50
|
-
只列出将要处理的文件,不 checkout。
|
|
51
|
-
|
|
52
|
-
.PARAMETER Tui
|
|
53
|
-
强制进入全屏 TUI(交互向导 + 进度面板)。
|
|
54
|
-
|
|
55
|
-
.PARAMETER NoTui
|
|
56
|
-
禁用 TUI,使用原来的纯日志输出(脚本/CI 推荐)。
|
|
57
|
-
|
|
58
|
-
.PARAMETER ResumeLast
|
|
59
|
-
从本机历史记录里恢复最近一次未完成(或最近一次)克隆。
|
|
60
|
-
|
|
61
|
-
.EXAMPLE
|
|
62
|
-
.\git-clone-resume.ps1 https://github.com/chaihahaha/git-cheatsheet.git
|
|
63
|
-
|
|
64
|
-
.EXAMPLE
|
|
65
|
-
.\git-clone-resume.ps1 https://github.com/user/repo.git -Ref main -OutDir D:\src\repo -BatchSize 64
|
|
66
|
-
|
|
67
|
-
.EXAMPLE
|
|
68
|
-
.\git-clone-resume.ps1 https://github.com/user/repo.git -Include src/* -Exclude *.bin
|
|
69
|
-
#>
|
|
70
|
-
[CmdletBinding()]
|
|
71
|
-
param(
|
|
72
|
-
[Parameter(Position = 0)]
|
|
73
|
-
[string]$RepoUrl,
|
|
74
|
-
|
|
75
|
-
[string]$OutDir,
|
|
76
|
-
|
|
77
|
-
[string]$Ref = "HEAD",
|
|
78
|
-
|
|
79
|
-
[ValidateRange(1, 5000)]
|
|
80
|
-
[int]$BatchSize = 32,
|
|
81
|
-
|
|
82
|
-
[ValidateRange(512, 30000)]
|
|
83
|
-
[int]$MaxArgChars = 6000,
|
|
84
|
-
|
|
85
|
-
[ValidateRange(1, 100)]
|
|
86
|
-
[int]$MaxRetries = 8,
|
|
87
|
-
|
|
88
|
-
[ValidateRange(0, 600)]
|
|
89
|
-
[int]$RetryDelaySeconds = 2,
|
|
90
|
-
|
|
91
|
-
[string[]]$Include,
|
|
92
|
-
|
|
93
|
-
[string[]]$Exclude,
|
|
94
|
-
|
|
95
|
-
[ValidateRange(1, 1000000)]
|
|
96
|
-
[int]$Depth,
|
|
97
|
-
|
|
98
|
-
[switch]$Verify,
|
|
99
|
-
|
|
100
|
-
[switch]$ForceRefetch,
|
|
101
|
-
|
|
102
|
-
[switch]$DryRun,
|
|
103
|
-
|
|
104
|
-
[switch]$Tui,
|
|
105
|
-
|
|
106
|
-
[switch]$NoTui,
|
|
107
|
-
|
|
108
|
-
[ValidateSet("zh-CN", "en-US")]
|
|
109
|
-
[string]$Language = "zh-CN",
|
|
110
|
-
|
|
111
|
-
[switch]$ResumeLast,
|
|
112
|
-
|
|
113
|
-
[switch]$Help
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
Set-StrictMode -Version Latest
|
|
117
|
-
$ErrorActionPreference = "Stop"
|
|
118
|
-
|
|
119
|
-
try { $null = cmd /c "chcp 65001 >NUL" } catch { }
|
|
120
|
-
try {
|
|
121
|
-
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
122
|
-
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
123
|
-
} catch { }
|
|
124
|
-
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
125
|
-
if (-not $env:LC_ALL) { $env:LC_ALL = "C.UTF-8" }
|
|
126
|
-
if (-not $env:LANG) { $env:LANG = "C.UTF-8" }
|
|
127
|
-
if (-not $env:GIT_HTTP_LOW_SPEED_LIMIT) { $env:GIT_HTTP_LOW_SPEED_LIMIT = "1024" }
|
|
128
|
-
if (-not $env:GIT_HTTP_LOW_SPEED_TIME) { $env:GIT_HTTP_LOW_SPEED_TIME = "60" }
|
|
129
|
-
$env:GIT_FLUSH = "1"
|
|
130
|
-
|
|
131
|
-
$script:Utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
|
132
|
-
$script:RepoRoot = $null
|
|
133
|
-
$script:LogFile = $null
|
|
134
|
-
$script:GitExe = $null
|
|
135
|
-
$script:StateDirName = "partial-resume"
|
|
136
|
-
$script:GcrTuiWanted = $false
|
|
137
|
-
$script:GcrExitCode = 0
|
|
138
|
-
$script:GcrUserStop = $false
|
|
139
|
-
$script:GcrLanguage = $Language
|
|
140
|
-
|
|
141
|
-
function Convert-GcrText {
|
|
142
|
-
param([AllowNull()][string]$Text)
|
|
143
|
-
if ($null -eq $Text -or $script:GcrLanguage -ne "en-US") { return $Text }
|
|
144
|
-
$
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
"
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
$
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
$
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if ($
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
$
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
$
|
|
604
|
-
$
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (
|
|
710
|
-
$
|
|
711
|
-
|
|
712
|
-
$
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
$
|
|
716
|
-
if ([string]::IsNullOrWhiteSpace($
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
return
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function
|
|
737
|
-
param([string]$RepoRoot
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
$
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
$
|
|
779
|
-
$
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
$
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function
|
|
791
|
-
param($
|
|
792
|
-
$
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
$
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
$
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
$
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
$
|
|
854
|
-
|
|
855
|
-
Invoke-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
$
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
$
|
|
868
|
-
if (
|
|
869
|
-
Write-Log
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function
|
|
879
|
-
param(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
$
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
$
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
$
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
$
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
$
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
$
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
$
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
}
|
|
989
|
-
$
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
$
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
$
|
|
1019
|
-
|
|
1020
|
-
$
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
$
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if (
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
if (
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
$
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
$
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
if (
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
}
|
|
1153
|
-
$
|
|
1154
|
-
$
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
$
|
|
1171
|
-
$
|
|
1172
|
-
$
|
|
1173
|
-
|
|
1174
|
-
$
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
$
|
|
1192
|
-
if (
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
$
|
|
1222
|
-
|
|
1223
|
-
$
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
$
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
$
|
|
1285
|
-
$
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
$
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
$
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Git 仓库 Windows 断点续传克隆(partial clone + 按批 checkout)。
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
针对 GitHub 等不稳定网络:先只拉 commit/tree 元数据(--filter=blob:none),
|
|
8
|
+
再分批把工作区文件 checkout 下来。中断后用同一命令重跑即可续传。
|
|
9
|
+
|
|
10
|
+
进度保存在仓库 .git/partial-resume/ 下,不污染工作区。
|
|
11
|
+
已成功落盘且大小(可选哈希)匹配的文件会自动跳过。
|
|
12
|
+
|
|
13
|
+
.PARAMETER RepoUrl
|
|
14
|
+
仓库地址。支持 https / ssh / git@ 以及本地路径。
|
|
15
|
+
|
|
16
|
+
.PARAMETER OutDir
|
|
17
|
+
本地目录。默认取 URL 最后一段(去掉 .git)。
|
|
18
|
+
|
|
19
|
+
.PARAMETER Ref
|
|
20
|
+
分支、标签或 commit。默认远程 HEAD。
|
|
21
|
+
|
|
22
|
+
.PARAMETER BatchSize
|
|
23
|
+
每批最多 checkout 的文件数。越大越快,中断粒度越粗。默认 32。
|
|
24
|
+
|
|
25
|
+
.PARAMETER MaxArgChars
|
|
26
|
+
单次 git 命令行参数最大字符数,避免超过 Windows 限制。默认 6000。
|
|
27
|
+
|
|
28
|
+
.PARAMETER MaxRetries
|
|
29
|
+
单个批次/文件失败后的最大重试次数。默认 8。
|
|
30
|
+
|
|
31
|
+
.PARAMETER RetryDelaySeconds
|
|
32
|
+
首次重试等待秒数,之后指数退避(封顶 60 秒)。默认 2。
|
|
33
|
+
|
|
34
|
+
.PARAMETER Include
|
|
35
|
+
只下载匹配这些通配符的路径(相对仓库根,支持 * 和 ?)。可重复。
|
|
36
|
+
|
|
37
|
+
.PARAMETER Exclude
|
|
38
|
+
排除匹配这些通配符的路径。可重复。
|
|
39
|
+
|
|
40
|
+
.PARAMETER Depth
|
|
41
|
+
可选浅克隆深度。不指定则拉完整 commit 历史(仍不拉 blob)。
|
|
42
|
+
|
|
43
|
+
.PARAMETER Verify
|
|
44
|
+
续传时对已存在文件做 hash-object 校验,哈希不一致则重下。
|
|
45
|
+
|
|
46
|
+
.PARAMETER ForceRefetch
|
|
47
|
+
强制重新 fetch 目标 ref(默认仅在本地还没有该 commit 时 fetch)。
|
|
48
|
+
|
|
49
|
+
.PARAMETER DryRun
|
|
50
|
+
只列出将要处理的文件,不 checkout。
|
|
51
|
+
|
|
52
|
+
.PARAMETER Tui
|
|
53
|
+
强制进入全屏 TUI(交互向导 + 进度面板)。
|
|
54
|
+
|
|
55
|
+
.PARAMETER NoTui
|
|
56
|
+
禁用 TUI,使用原来的纯日志输出(脚本/CI 推荐)。
|
|
57
|
+
|
|
58
|
+
.PARAMETER ResumeLast
|
|
59
|
+
从本机历史记录里恢复最近一次未完成(或最近一次)克隆。
|
|
60
|
+
|
|
61
|
+
.EXAMPLE
|
|
62
|
+
.\git-clone-resume.ps1 https://github.com/chaihahaha/git-cheatsheet.git
|
|
63
|
+
|
|
64
|
+
.EXAMPLE
|
|
65
|
+
.\git-clone-resume.ps1 https://github.com/user/repo.git -Ref main -OutDir D:\src\repo -BatchSize 64
|
|
66
|
+
|
|
67
|
+
.EXAMPLE
|
|
68
|
+
.\git-clone-resume.ps1 https://github.com/user/repo.git -Include src/* -Exclude *.bin
|
|
69
|
+
#>
|
|
70
|
+
[CmdletBinding()]
|
|
71
|
+
param(
|
|
72
|
+
[Parameter(Position = 0)]
|
|
73
|
+
[string]$RepoUrl,
|
|
74
|
+
|
|
75
|
+
[string]$OutDir,
|
|
76
|
+
|
|
77
|
+
[string]$Ref = "HEAD",
|
|
78
|
+
|
|
79
|
+
[ValidateRange(1, 5000)]
|
|
80
|
+
[int]$BatchSize = 32,
|
|
81
|
+
|
|
82
|
+
[ValidateRange(512, 30000)]
|
|
83
|
+
[int]$MaxArgChars = 6000,
|
|
84
|
+
|
|
85
|
+
[ValidateRange(1, 100)]
|
|
86
|
+
[int]$MaxRetries = 8,
|
|
87
|
+
|
|
88
|
+
[ValidateRange(0, 600)]
|
|
89
|
+
[int]$RetryDelaySeconds = 2,
|
|
90
|
+
|
|
91
|
+
[string[]]$Include,
|
|
92
|
+
|
|
93
|
+
[string[]]$Exclude,
|
|
94
|
+
|
|
95
|
+
[ValidateRange(1, 1000000)]
|
|
96
|
+
[int]$Depth,
|
|
97
|
+
|
|
98
|
+
[switch]$Verify,
|
|
99
|
+
|
|
100
|
+
[switch]$ForceRefetch,
|
|
101
|
+
|
|
102
|
+
[switch]$DryRun,
|
|
103
|
+
|
|
104
|
+
[switch]$Tui,
|
|
105
|
+
|
|
106
|
+
[switch]$NoTui,
|
|
107
|
+
|
|
108
|
+
[ValidateSet("zh-CN", "en-US")]
|
|
109
|
+
[string]$Language = "zh-CN",
|
|
110
|
+
|
|
111
|
+
[switch]$ResumeLast,
|
|
112
|
+
|
|
113
|
+
[switch]$Help
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
Set-StrictMode -Version Latest
|
|
117
|
+
$ErrorActionPreference = "Stop"
|
|
118
|
+
|
|
119
|
+
try { $null = cmd /c "chcp 65001 >NUL" } catch { }
|
|
120
|
+
try {
|
|
121
|
+
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
122
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
123
|
+
} catch { }
|
|
124
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
125
|
+
if (-not $env:LC_ALL) { $env:LC_ALL = "C.UTF-8" }
|
|
126
|
+
if (-not $env:LANG) { $env:LANG = "C.UTF-8" }
|
|
127
|
+
if (-not $env:GIT_HTTP_LOW_SPEED_LIMIT) { $env:GIT_HTTP_LOW_SPEED_LIMIT = "1024" }
|
|
128
|
+
if (-not $env:GIT_HTTP_LOW_SPEED_TIME) { $env:GIT_HTTP_LOW_SPEED_TIME = "60" }
|
|
129
|
+
$env:GIT_FLUSH = "1"
|
|
130
|
+
|
|
131
|
+
$script:Utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
|
132
|
+
$script:RepoRoot = $null
|
|
133
|
+
$script:LogFile = $null
|
|
134
|
+
$script:GitExe = $null
|
|
135
|
+
$script:StateDirName = "partial-resume"
|
|
136
|
+
$script:GcrTuiWanted = $false
|
|
137
|
+
$script:GcrExitCode = 0
|
|
138
|
+
$script:GcrUserStop = $false
|
|
139
|
+
$script:GcrLanguage = $Language
|
|
140
|
+
|
|
141
|
+
function Convert-GcrText {
|
|
142
|
+
param([AllowNull()][string]$Text)
|
|
143
|
+
if ($null -eq $Text -or $script:GcrLanguage -ne "en-US") { return $Text }
|
|
144
|
+
$full = [ordered]@{
|
|
145
|
+
"断点续传克隆 · partial clone + 按批 checkout" = "Resumable clone · partial clone + batch checkout"
|
|
146
|
+
"选择界面语言。可随时按 L 在中文和英文之间切换。" = "Interface language. Press L to switch between Chinese and English."
|
|
147
|
+
"续传时对已有文件做 hash-object 校验,哈希不一致则重新下载。Space 开关。" = "Verify existing files with hash-object when resuming. Re-download mismatches. Space toggles."
|
|
148
|
+
"浅克隆深度。留空则拉完整 commit 历史(仍然不拉 blob)。" = "Shallow clone depth. Leave empty for full commit history (blobs are still deferred)."
|
|
149
|
+
"跳过匹配的路径,例如 *.bin,*.zip。可与「只含路径」同时使用。" = "Skip matching paths, such as *.bin,*.zip. Can be combined with Include paths."
|
|
150
|
+
"只下载匹配的路径,逗号分隔通配符,例如 src/*,docs/*。空表示全部。" = "Download only matching paths, comma-separated patterns such as src/*,docs/*. Empty means all."
|
|
151
|
+
"单文件失败后的最大重试次数。← → 调整。网络不稳时可调大。" = "Maximum retries after a single-file failure. Use Left/Right to adjust. Increase for unstable networks."
|
|
152
|
+
"每批 checkout 的文件数。越大越快,中断粒度越粗。← → 调整。" = "Files checked out per batch. Larger is faster but less granular. Use Left/Right to adjust."
|
|
153
|
+
"分支、标签或 commit SHA。默认远程 HEAD。" = "Branch, tag, or commit SHA. Defaults to remote HEAD."
|
|
154
|
+
"远程仓库地址,支持 https、ssh、git@ 以及本地路径。Ctrl+V 从剪贴板粘贴。" = "Remote repository URL. Supports https, ssh, git@, and local paths. Ctrl+V pastes from the clipboard."
|
|
155
|
+
"工作区目录。留空则用仓库名。已有 .git/partial-resume 时自动续传。" = "Workspace directory. Leave empty to use the repository name. Existing .git/partial-resume state resumes automatically."
|
|
156
|
+
"强制重新 fetch 目标 ref。换分支或更新到最新 commit 时打开。Space 开关。" = "Force-fetch the target ref. Enable when changing branches or updating to the latest commit. Space toggles."
|
|
157
|
+
"只列出将要处理的文件,不下载 blob。适合先看清单。Space 开关。" = "List files without downloading blobs. Useful for previewing the file list. Space toggles."
|
|
158
|
+
"按上面的设置开始或继续克隆。Enter 启动。中断后重跑即可续传。" = "Start or resume with the settings above. Press Enter to begin; rerun after interruption."
|
|
159
|
+
"将在当前 git 命令结束后停止。Ctrl+C 再按一次立即结束。" = "Stopping after the current git command. Press Ctrl+C again to force stop."
|
|
160
|
+
"已暂停:当前批次结束后停住。Space 继续,Q 停止。" = "Paused after the current batch. Space resumes; Q stops."
|
|
161
|
+
"正在拉取 commit/tree 元数据(blob:none)。文件内容会在下一步按批下载。" = "Fetching commit/tree metadata (blob:none). File contents download in batches next."
|
|
162
|
+
"按批 checkout 文件。中断后重跑同一命令即可续传。" = "Check out files in batches. Rerun the same command after an interruption to resume."
|
|
163
|
+
"脚本模式:加 -NoTui。强制界面:加 -Tui。" = "Script mode: add -NoTui. Force the interface: add -Tui."
|
|
164
|
+
"续传:重新运行同一条命令。进度在 .git/partial-resume/" = "Resume: run the same command again. Progress is stored in .git/partial-resume/"
|
|
165
|
+
"任意键关闭帮助" = "Press any key to close help"
|
|
166
|
+
"当前 git 命令结束后生效;再按一次强制结束" = "takes effect after the current git command; press again to force stop"
|
|
167
|
+
"当前批次结束后暂停" = "pause after the current batch"
|
|
168
|
+
"从暂停恢复" = "resume from pause"
|
|
169
|
+
"切换失败文件列表" = "toggle failed files"
|
|
170
|
+
"滚动活动日志" = "scroll activity log"
|
|
171
|
+
"跟随最新日志" = "follow latest log"
|
|
172
|
+
"打开或关闭本帮助" = "toggle this help"
|
|
173
|
+
"键盘" = "Keyboard"
|
|
174
|
+
"仓库" = "Repository"
|
|
175
|
+
"目录" = "Directory"
|
|
176
|
+
"引用" = "Ref"
|
|
177
|
+
"阶段" = "Phase"
|
|
178
|
+
"当前" = "Current"
|
|
179
|
+
"失败" = "failed"
|
|
180
|
+
"批次" = "batch"
|
|
181
|
+
"暂停" = "pause"
|
|
182
|
+
"停止" = "stop"
|
|
183
|
+
"帮助" = "help"
|
|
184
|
+
"日志" = "log"
|
|
185
|
+
"继续" = "resume"
|
|
186
|
+
"(无。完成一次克隆后会出现在这里)" = "(None. Completed clones appear here)"
|
|
187
|
+
"初始化本地仓库" = "Initialize repository"
|
|
188
|
+
"快捷键说明。任意键关闭此帮助。" = "Keyboard help. Press any key to close."
|
|
189
|
+
"失败文件列表。F 返回活动日志,再次运行同一命令会重试。" = "Failed files. Press F to return to the activity log; run the same command to retry."
|
|
190
|
+
"初始化本地仓库并配置 partial clone(只拉元数据,不拉文件内容)。" = "Initialize the local repository and configure partial clone (metadata only, no file contents)."
|
|
191
|
+
"枚举仓库文件树。不会为了拿大小去拉全部 blob。" = "List the repository file tree without downloading all blobs just to determine their sizes."
|
|
192
|
+
"扫描工作区,跳过已经落盘的文件,其余进入待下载队列。" = "Scan the workspace, skip files already on disk, and queue the rest for download."
|
|
193
|
+
"全部完成。工作区已可用。" = "Everything is complete. The workspace is ready."
|
|
194
|
+
"出错或未完成。重新运行同一命令即可从断点继续。" = "Failed or incomplete. Run the same command to resume."
|
|
195
|
+
"退出向导?未开始的克隆不会写入进度。Enter 确定,Esc 取消。" = "Quit the wizard? No progress is written before a clone starts. Enter confirms; Esc cancels."
|
|
196
|
+
"正在编辑。Enter 确认,Esc 取消,Ctrl+V 粘贴。光标用 ← → Home End。" = "Editing. Enter confirms, Esc cancels, Ctrl+V pastes. Move with Left/Right, Home, or End."
|
|
197
|
+
"最近任务。Enter 填入 URL/目录/分支,可直接续传未完成的克隆。" = "Recent tasks. Enter fills in the URL, directory, and branch to resume an incomplete clone."
|
|
198
|
+
"暂无失败文件。" = "No failed files."
|
|
199
|
+
" Enter 关闭 · Q 退出 · ? 帮助" = " Enter close · Q quit · ? help"
|
|
200
|
+
" Ctrl+C 再按一次强制结束 · ? 帮助" = " Ctrl+C again to force stop · ? help"
|
|
201
|
+
" Enter 编辑/开始 · Space 开关 · ←→ 改批次 · Ctrl+V 粘贴 · Q 退出" = " Enter edit/start · Space toggle · Left/Right batch · Ctrl+V paste · Q quit"
|
|
202
|
+
" Enter 确定退出 · Esc 返回" = " Enter confirm quit · Esc back"
|
|
203
|
+
"本地目录" = "Local directory"
|
|
204
|
+
"请填写仓库 URL(Ctrl+V 可从剪贴板粘贴)" = "Enter a repository URL (Ctrl+V pastes from the clipboard)"
|
|
205
|
+
"Enter 编辑 · Space 开关 · Tab 最近任务 · S 开始 · Q 退出" = "Enter edit · Space toggle · Tab recent tasks · S start · Q quit"
|
|
206
|
+
"git-clone-resume 交互设置" = "git-clone-resume interactive setup"
|
|
207
|
+
"直接回车使用括号里的默认值。空 URL 则退出。" = "Press Enter to use the defaults in brackets. An empty URL exits."
|
|
208
|
+
'每批文件数 [$batch]' = 'Files per batch [$batch]'
|
|
209
|
+
'分支/标签/commit [$defRef]' = 'Branch/tag/commit [$defRef]'
|
|
210
|
+
'本地目录 [$defDir]' = 'Local directory [$defDir]'
|
|
211
|
+
}
|
|
212
|
+
$specific = [ordered]@{
|
|
213
|
+
"断点续传克隆。Q 停止 · P 暂停 · ? 帮助" = "Resumable clone. Q stop · P pause · ? help"
|
|
214
|
+
"初始化本地仓库并配置 partial clone(只拉元数据,不拉文件内容)。" = "Initialize the local repository and configure partial clone (metadata only, no file contents)."
|
|
215
|
+
"枚举仓库文件树。不会为了拿大小去拉全部 blob。" = "List the repository file tree without downloading all blobs just to determine their sizes."
|
|
216
|
+
"修复 Windows 上可能被弄乱的 git index。" = "Repair the Git index if Windows left it inconsistent."
|
|
217
|
+
"出错或未完成。重新运行同一命令即可从断点继续。" = "Failed or incomplete. Run the same command to resume."
|
|
218
|
+
"最近任务。Enter 填入 URL/目录/分支,可直接续传未完成的克隆。" = "Recent tasks. Enter fills in the URL, directory, and branch to resume an incomplete clone."
|
|
219
|
+
"↑↓ 选择选项,Enter 编辑或开始。每个选项的说明会显示在这一行。" = "Up/Down select; Enter edits or starts. The selected option's guide appears here."
|
|
220
|
+
" Enter 关闭 · Q 退出 · ? 帮助" = " Enter close · Q quit · ? help"
|
|
221
|
+
" Ctrl+C 再按一次强制结束 · ? 帮助" = " Ctrl+C again to force stop · ? help"
|
|
222
|
+
" 最近任务 (Tab 切换 · Enter 填入)" = " Recent tasks (Tab switch · Enter fill in)"
|
|
223
|
+
" Enter 编辑/开始 · Space 开关 · ←→ 改批次 · Ctrl+V 粘贴 · Q 退出" = " Enter edit/start · Space toggle · Left/Right batch · Ctrl+V paste · Q quit"
|
|
224
|
+
" Enter 确认 · Esc 取消 · Ctrl+V 粘贴" = " Enter confirm · Esc cancel · Ctrl+V paste"
|
|
225
|
+
" Enter 确定退出 · Esc 返回" = " Enter confirm quit · Esc back"
|
|
226
|
+
"初始化仓库" = "Initialize repository"
|
|
227
|
+
"拉取元数据" = "Fetch metadata"
|
|
228
|
+
"枚举文件树" = "List file tree"
|
|
229
|
+
"扫描已有文件" = "Scan existing files"
|
|
230
|
+
"下载文件" = "Download files"
|
|
231
|
+
"修复 git index" = "Repair Git index"
|
|
232
|
+
"就绪" = "Ready"
|
|
233
|
+
"设置" = "Setup"
|
|
234
|
+
"出错" = "Error"
|
|
235
|
+
"最近任务" = "Recent tasks"
|
|
236
|
+
"暂无失败文件。" = "No failed files."
|
|
237
|
+
"快捷键说明。任意键关闭此帮助。" = "Keyboard help. Press any key to close."
|
|
238
|
+
"失败文件列表。F 返回活动日志,再次运行同一命令会重试。" = "Failed files. Press F to return to the activity log; run the same command to retry."
|
|
239
|
+
"本次运行已结束。Enter 关闭界面,进度保留在 .git/partial-resume/。" = "This run has ended. Press Enter to close; progress remains in .git/partial-resume/."
|
|
240
|
+
"请填写仓库 URL(Ctrl+V 可从剪贴板粘贴)" = "Enter a repository URL (Ctrl+V pastes from the clipboard)"
|
|
241
|
+
"无法开始: " = "Could not start: "
|
|
242
|
+
'本地目录 [$defDir]' = 'Local directory [$defDir]'
|
|
243
|
+
'分支/标签/commit [$defRef]' = 'Branch/tag/commit [$defRef]'
|
|
244
|
+
'每批文件数 [$batch]' = 'Files per batch [$batch]'
|
|
245
|
+
"本地目录" = "Local directory"
|
|
246
|
+
"分支/标签" = "Branch/tag"
|
|
247
|
+
"每批文件" = "Files per batch"
|
|
248
|
+
"重试次数" = "Retries"
|
|
249
|
+
"只含路径" = "Include paths"
|
|
250
|
+
"排除路径" = "Exclude paths"
|
|
251
|
+
"浅克隆深度" = "Clone depth"
|
|
252
|
+
"哈希校验" = "Hash verification"
|
|
253
|
+
"强制 refetch" = "Force refetch"
|
|
254
|
+
"开始克隆" = "Start clone"
|
|
255
|
+
"(自动)" = "(auto)"
|
|
256
|
+
"(全部)" = "(all)"
|
|
257
|
+
"(无)" = "(none)"
|
|
258
|
+
"(完整历史)" = "(full history)"
|
|
259
|
+
"{0} 分钟前" = "{0} minutes ago"
|
|
260
|
+
"{0} 小时前" = "{0} hours ago"
|
|
261
|
+
"{0} 天前" = "{0} days ago"
|
|
262
|
+
"全部完成。工作区已可用。" = "Everything is complete. The workspace is ready."
|
|
263
|
+
"(无。完成一次克隆后会出现在这里)" = "(None. Completed clones appear here)"
|
|
264
|
+
"单文件失败后的最大重试次数。← → 调整。网络不稳时可调大。" = "Maximum retries after a single-file failure. Use Left/Right to adjust. Increase for unstable networks."
|
|
265
|
+
"跳过匹配的路径,例如 *.bin,*.zip。可与「只含路径」同时使用。" = "Skip matching paths, such as *.bin,*.zip. Can be combined with Include paths."
|
|
266
|
+
"浅克隆深度。留空则拉完整 commit 历史(仍然不拉 blob)。" = "Shallow clone depth. Leave empty for full commit history (blobs are still deferred)."
|
|
267
|
+
"选择界面语言。可随时按 L 在中文和英文之间切换。" = "Interface language. Press L to switch between Chinese and English."
|
|
268
|
+
"按上面的设置开始或继续克隆。Enter 启动。中断后重跑即可续传。" = "Start or resume with the settings above. Press Enter to begin; rerun after interruption."
|
|
269
|
+
"Enter 编辑 · Space 开关 · Tab 最近任务 · S 开始 · Q 退出" = "Enter edit · Space toggle · Tab recent tasks · S start · Q quit"
|
|
270
|
+
"git-clone-resume 交互设置" = "git-clone-resume interactive setup"
|
|
271
|
+
"语言" = "Language"
|
|
272
|
+
"英文" = "English"
|
|
273
|
+
"中文" = "Chinese"
|
|
274
|
+
"开" = "On"
|
|
275
|
+
"关" = "Off"
|
|
276
|
+
}
|
|
277
|
+
$translations = @{}
|
|
278
|
+
foreach ($key in $full.Keys) { $translations[$key] = [string]$full[$key] }
|
|
279
|
+
foreach ($key in $specific.Keys) { $translations[$key] = [string]$specific[$key] }
|
|
280
|
+
foreach ($key in @($translations.Keys | Sort-Object Length -Descending)) {
|
|
281
|
+
$Text = $Text.Replace($key, $translations[$key])
|
|
282
|
+
}
|
|
283
|
+
$pairs = @(
|
|
284
|
+
@("未找到 git。请先安装 Git for Windows", "Git was not found. Install Git for Windows"),
|
|
285
|
+
@("错误:", "Error:"), @("必须提供仓库 URL。", "A repository URL is required."),
|
|
286
|
+
@("无法读取历史记录。", "Could not read history."),
|
|
287
|
+
@("没有可恢复的历史记录。请先启动过一次克隆。", "No resumable history was found. Start a clone first."),
|
|
288
|
+
@("向导失败:", "Wizard failed:"), @("无法开始克隆", "Could not start clone"),
|
|
289
|
+
@("当前终端无法进入全屏 TUI,改用日志模式。Windows Terminal 下再试,或去掉 -Tui。", "This terminal cannot enter fullscreen TUI; using log mode. Try Windows Terminal or remove -Tui."),
|
|
290
|
+
@("已由用户停止。", "Stopped by user."), @("再次运行同一命令即可续传。", "Run the same command again to resume."),
|
|
291
|
+
@("使用 ", "Using "), @("仓库:", "Repository:"), @("目录:", "Directory:"), @("引用:", "Ref:"),
|
|
292
|
+
@("语言", "Language"), @("中文", "Chinese"), @("英文", "English"),
|
|
293
|
+
@("目标 commit:", "Target commit:"), @("fetch 元数据:", "Fetching metadata:"), @("枚举文件树", "Enumerating file tree"),
|
|
294
|
+
@("树中条目:", "Tree entries:"), @("待处理文件:", "Pending files:"), @("工作区:", "Workspace:"),
|
|
295
|
+
@("全部文件已就绪。", "All files are ready."), @("完成", "Complete"), @("克隆完成", "Clone complete"),
|
|
296
|
+
@("部分文件失败", "Some files failed"), @("失败列表:", "Failure list:"), @("仍失败:", "Still failed:"),
|
|
297
|
+
@("批次", "Batch"), @("失败", "failed"), @("单文件失败:", "Single-file failure:"), @("出错", "Error"),
|
|
298
|
+
@("已停止", "Stopped"), @("中断后续传: ", "Resume after interruption: "), @("仓库目录:", "Repository directory:"),
|
|
299
|
+
@("文件数:", "Files:"), @("清单文件:", "List file:"), @("DryRun 完成", "DryRun complete"),
|
|
300
|
+
@("DryRun 结束,清单:", "DryRun finished; list:"),
|
|
301
|
+
@("(blob 体积在 checkout 后统计,避免 ls-tree -l 把全部 blob 拉下来)", "(blob sizes are measured during checkout; ls-tree -l is avoided to prevent downloading all blobs)"),
|
|
302
|
+
@("未下载 blob。去掉 -DryRun 后开始/继续克隆。", "No blobs were downloaded. Remove -DryRun to start or resume."),
|
|
303
|
+
@("检查工作区已有文件", "Checking existing workspace files"), @("进度: 已记录", "Progress: recorded"),
|
|
304
|
+
@("分 ", "Downloading in "), @(" 批下载,每批最多 ", " batches, up to "), @(" 个文件", " files each"),
|
|
305
|
+
@("跳过", "Skipped"), @("个子模块(gitlink)。需要的话请在对应目录单独再跑本脚本。", " submodules (gitlinks). Run this script separately if needed.")
|
|
306
|
+
,@("断点续传克隆", "Resumable clone"), @("按批 checkout", "batch checkout"), @("刚刚", "just now"),
|
|
307
|
+
@("分钟前", " minutes ago"), @("小时前", " hours ago"), @("天前", " days ago"),
|
|
308
|
+
@("就绪", "Ready"), @("设置", "Setup"), @("初始化仓库", "Initialize repository"), @("拉取元数据", "Fetch metadata"),
|
|
309
|
+
@("枚举文件树", "List file tree"), @("扫描已有文件", "Scan existing files"), @("下载文件", "Download files"),
|
|
310
|
+
@("修复 git index", "Repair git index"), @("快捷键说明。任意键关闭此帮助。", "Keyboard help. Press any key to close."),
|
|
311
|
+
@("失败文件列表。F 返回活动日志,再次运行同一命令会重试。", "Failed files. Press F to return; run the same command to retry."),
|
|
312
|
+
@("本次运行已结束。Enter 关闭界面,进度保留在 .git/partial-resume/。", "This run has ended. Press Enter to close; progress remains in .git/partial-resume/."),
|
|
313
|
+
@("已暂停:当前批次结束后停住。Space 继续,Q 停止。", "Paused after the current batch. Space resumes; Q stops."),
|
|
314
|
+
@("全部完成。工作区已可用。", "Everything is complete. The workspace is ready."),
|
|
315
|
+
@("出错或未完成。重新运行同一命令即可从断点继续。", "Failed or incomplete. Run the same command to resume."),
|
|
316
|
+
@("远程仓库地址,支持 https、ssh、git@ 以及本地路径。Ctrl+V 从剪贴板粘贴。", "Remote repository URL. Supports https, ssh, git@, and local paths. Ctrl+V pastes from the clipboard."),
|
|
317
|
+
@("工作区目录。留空则用仓库名。已有 .git/partial-resume 时自动续传。", "Workspace directory. Leave empty to use the repository name. Existing .git/partial-resume state resumes automatically."),
|
|
318
|
+
@("选择界面语言。可随时按 L 在中文和英文之间切换。", "Interface language. Press L at any time to switch between Chinese and English."),
|
|
319
|
+
@("最近任务", "Recent tasks"), @("切换", "switch"), @("填入", "fill in"), @("无。完成一次克隆后会出现在这里", "None. Completed clones appear here"),
|
|
320
|
+
@("Enter 编辑/开始", "Enter edit/start"), @("Space 开关", "Space toggle"), @("改批次", "change batch"), @("粘贴", "paste"), @("退出", "quit"),
|
|
321
|
+
@("按上面的设置开始或继续克隆。Enter 启动。中断后重跑即可续传。", "Start or resume with the settings above. Press Enter to begin; rerun after interruption."),
|
|
322
|
+
@("请填写仓库 URL(Ctrl+V 可从剪贴板粘贴)", "Enter a repository URL (Ctrl+V pastes from the clipboard)"),
|
|
323
|
+
@("语言", "Language"), @("中文", "Chinese"), @("英文", "English")
|
|
324
|
+
,@(" 仓库 ", " Repository "), @(" 目录 ", " Directory "), @(" 引用 ", " Ref "), @(" 阶段 ", " Phase "), @(" 当前 ", " Current "),
|
|
325
|
+
@("键盘", "Keyboard"), @("暂无失败文件。", "No failed files."), @("停止", "stop"), @("暂停", "pause"), @("帮助", "help"), @("日志", "log")
|
|
326
|
+
,@("仓库 URL", "Repository URL"), @("本地目录", "Local directory"), @("分支/标签", "Branch/tag"), @("每批文件", "Files per batch"),
|
|
327
|
+
@("重试次数", "Retries"), @("只含路径", "Include paths"), @("排除路径", "Exclude paths"), @("浅克隆深度", "Clone depth"),
|
|
328
|
+
@("哈希校验", "Hash verification"), @("强制 refetch", "Force refetch"), @("开始克隆", "Start clone"),
|
|
329
|
+
@("(自动)", "(auto)"), @("(全部)", "(all)"), @("(无)", "(none)"), @("(完整历史)", "(full history)"),
|
|
330
|
+
@("强制重新 fetch 目标 ref。换分支或更新到最新 commit 时打开。Space 开关。", "Force-fetch the target ref. Enable when changing branches or updating to the latest commit. Space toggles."),
|
|
331
|
+
@("只列出将要处理的文件,不下载 blob。适合先看清单。Space 开关。", "List files without downloading blobs. Useful for previewing the file list. Space toggles."),
|
|
332
|
+
@("按上面的设置开始或继续克隆。Enter 启动。中断后重跑即可续传。", "Start or resume with the settings above. Press Enter to begin; rerun after interruption."),
|
|
333
|
+
@("Enter 编辑/开始", "Enter edit/start"), @("Space 开关", "Space toggle"), @("←→ 改批次", "Left/Right change batch"),
|
|
334
|
+
@("Ctrl+V 粘贴", "Ctrl+V paste"), @("Q 退出", "Q quit"), @("Enter 确认", "Enter confirm"), @("Esc 取消", "Esc cancel"),
|
|
335
|
+
@("退出向导?未开始的克隆不会写入进度。Enter 确定,Esc 取消。", "Quit the wizard? No progress is written before a clone starts. Enter confirms; Esc cancels."),
|
|
336
|
+
@("↑↓ 选择选项,Enter 编辑或开始。每个选项的说明会显示在这一行。", "Up/Down select; Enter edits or starts. The selected option's guide appears here."),
|
|
337
|
+
@("将在当前 git 命令结束后停止。Ctrl+C 再按一次立即结束。", "Stopping after the current git command. Press Ctrl+C again to force stop."),
|
|
338
|
+
@("初始化本地仓库并配置 partial clone(只拉元数据,不拉文件内容)。", "Initialize the repository and configure partial clone (metadata only)."),
|
|
339
|
+
@("正在拉取 commit/tree 元数据(blob:none)。文件内容会在下一步按批下载。", "Fetching commit/tree metadata (blob:none). File contents download in batches next."),
|
|
340
|
+
@("扫描工作区,跳过已经落盘的文件,其余进入待下载队列。", "Scan the workspace, skip files already on disk, and queue the rest."),
|
|
341
|
+
@("按批 checkout 文件。中断后重跑同一命令即可续传。", "Check out files in batches. Rerun the same command after an interruption to resume."),
|
|
342
|
+
@("修复 Windows 上可能被弄乱的 git index。", "Repair the Git index if Windows left it inconsistent."),
|
|
343
|
+
@("Q 停止 · P 暂停 · ? 帮助", "Q stop · P pause · ? help")
|
|
344
|
+
)
|
|
345
|
+
$result = $Text
|
|
346
|
+
$direct = [ordered]@{
|
|
347
|
+
"断点续传克隆" = "Resumable clone"
|
|
348
|
+
"按批 checkout" = "batch checkout"
|
|
349
|
+
"仓库 URL" = "Repository URL"
|
|
350
|
+
"本地目录" = "Local directory"
|
|
351
|
+
"分支/标签" = "Branch/tag"
|
|
352
|
+
"每批文件" = "Files per batch"
|
|
353
|
+
"重试次数" = "Retries"
|
|
354
|
+
"只含路径" = "Include paths"
|
|
355
|
+
"排除路径" = "Exclude paths"
|
|
356
|
+
"浅克隆深度" = "Clone depth"
|
|
357
|
+
"哈希校验" = "Hash verification"
|
|
358
|
+
"开始克隆" = "Start clone"
|
|
359
|
+
"强制重新 fetch 目标 ref。换分支或更新到最新 commit 时打开。Space 开关。" = "Force-fetch the target ref. Enable when changing branches or updating to the latest commit. Space toggles."
|
|
360
|
+
"只列出将要处理的文件,不下载 blob。适合先看清单。Space 开关。" = "List files without downloading blobs. Useful for previewing the file list. Space toggles."
|
|
361
|
+
"按上面的设置开始或继续克隆。Enter 启动。中断后重跑即可续传。" = "Start or resume with the settings above. Press Enter to begin; rerun after interruption."
|
|
362
|
+
"Enter 编辑/开始" = "Enter edit/start"
|
|
363
|
+
"Space 开关" = "Space toggle"
|
|
364
|
+
"←→ 改批次" = "Left/Right change batch"
|
|
365
|
+
"Ctrl+V 粘贴" = "Ctrl+V paste"
|
|
366
|
+
"Q 退出" = "Q quit"
|
|
367
|
+
"Language" = "Language"
|
|
368
|
+
}
|
|
369
|
+
foreach ($key in $direct.Keys) { $result = $result.Replace($key, [string]$direct[$key]) }
|
|
370
|
+
$items = @($pairs)
|
|
371
|
+
for ($i = 0; $i -lt $items.Count;) {
|
|
372
|
+
if ($items[$i] -is [array] -and @($items[$i]).Count -ge 2) {
|
|
373
|
+
$from = [string]@($items[$i])[0]
|
|
374
|
+
$to = [string]@($items[$i])[1]
|
|
375
|
+
$i++
|
|
376
|
+
} elseif ($i + 1 -lt $items.Count) {
|
|
377
|
+
$from = [string]$items[$i]
|
|
378
|
+
$to = [string]$items[$i + 1]
|
|
379
|
+
$i += 2
|
|
380
|
+
} else {
|
|
381
|
+
break
|
|
382
|
+
}
|
|
383
|
+
if (-not [string]::IsNullOrEmpty($from)) { $result = $result.Replace($from, $to) }
|
|
384
|
+
}
|
|
385
|
+
return $result
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
$script:GcrTuiFile = Join-Path $PSScriptRoot "git-clone-resume.tui.ps1"
|
|
389
|
+
if (-not $PSScriptRoot) {
|
|
390
|
+
$script:GcrTuiFile = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "git-clone-resume.tui.ps1"
|
|
391
|
+
}
|
|
392
|
+
if (Test-Path -LiteralPath $script:GcrTuiFile) {
|
|
393
|
+
. $script:GcrTuiFile
|
|
394
|
+
} else {
|
|
395
|
+
function Test-GcrTuiActive { return $false }
|
|
396
|
+
function Test-GcrTuiAvailable { return $false }
|
|
397
|
+
function Test-GcrTuiQuit { return $false }
|
|
398
|
+
function Test-GcrTuiForceQuit { return $false }
|
|
399
|
+
function Invoke-GcrTuiTick { }
|
|
400
|
+
function Write-GcrNewline { Write-Host "" }
|
|
401
|
+
function Initialize-GcrTui { return $false }
|
|
402
|
+
function Close-GcrTui { }
|
|
403
|
+
function Set-GcrTuiPhase { }
|
|
404
|
+
function Set-GcrTuiRepo { }
|
|
405
|
+
function Update-GcrTuiProgress { }
|
|
406
|
+
function Add-GcrTuiLog { }
|
|
407
|
+
function Add-GcrTuiFailure { }
|
|
408
|
+
function Add-GcrGitOutput { }
|
|
409
|
+
function Receive-GcrGitBytes { }
|
|
410
|
+
function Wait-GcrTuiPaused { }
|
|
411
|
+
function Show-GcrTuiResult { }
|
|
412
|
+
function Save-GcrHistory { }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (-not $PSBoundParameters.ContainsKey("Language") -and (Get-Command Get-GcrLanguagePreference -ErrorAction SilentlyContinue)) {
|
|
416
|
+
$savedLanguage = Get-GcrLanguagePreference
|
|
417
|
+
if ($savedLanguage) { $script:GcrLanguage = $savedLanguage }
|
|
418
|
+
}
|
|
419
|
+
if ($PSBoundParameters.ContainsKey("Language") -and (Get-Command Save-GcrLanguagePreference -ErrorAction SilentlyContinue)) {
|
|
420
|
+
Save-GcrLanguagePreference -Language $Language
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function Write-Log {
|
|
424
|
+
param(
|
|
425
|
+
$Message,
|
|
426
|
+
[string]$Level = "INFO"
|
|
427
|
+
)
|
|
428
|
+
if (@("INFO", "WARN", "ERROR", "OK", "STEP") -notcontains $Level) { $Level = "INFO" }
|
|
429
|
+
$text = ""
|
|
430
|
+
try {
|
|
431
|
+
if ($null -eq $Message) { $text = "" }
|
|
432
|
+
elseif ($Message -is [System.Array]) { $text = (@($Message | ForEach-Object { "$_" }) -join " ") }
|
|
433
|
+
else { $text = [string]$Message }
|
|
434
|
+
} catch { $text = "$Message" }
|
|
435
|
+
$text = Convert-GcrText $text
|
|
436
|
+
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
437
|
+
$color = switch ($Level) {
|
|
438
|
+
"INFO" { "Gray" }
|
|
439
|
+
"WARN" { "Yellow" }
|
|
440
|
+
"ERROR" { "Red" }
|
|
441
|
+
"OK" { "Green" }
|
|
442
|
+
"STEP" { "Cyan" }
|
|
443
|
+
default { "Gray" }
|
|
444
|
+
}
|
|
445
|
+
$line = "[$ts][$Level] $text"
|
|
446
|
+
try {
|
|
447
|
+
if (Test-GcrTuiActive) {
|
|
448
|
+
Add-GcrTuiLog -Level $Level -Message $text
|
|
449
|
+
Invoke-GcrTuiTick
|
|
450
|
+
} else {
|
|
451
|
+
Write-Host $line -ForegroundColor $color
|
|
452
|
+
}
|
|
453
|
+
} catch { }
|
|
454
|
+
if ($script:LogFile) {
|
|
455
|
+
try {
|
|
456
|
+
[System.IO.File]::AppendAllText($script:LogFile, $line + [Environment]::NewLine, $script:Utf8NoBom)
|
|
457
|
+
} catch { }
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function Show-Usage {
|
|
462
|
+
Write-Host "Git resume clone for Windows / PowerShell 5.1+"
|
|
463
|
+
Write-Host ""
|
|
464
|
+
Write-Host "Usage:"
|
|
465
|
+
Write-Host " .\git-clone-resume.ps1 <repo-url> [options]"
|
|
466
|
+
Write-Host " git-clone-resume.cmd <repo-url> [options]"
|
|
467
|
+
Write-Host ""
|
|
468
|
+
Write-Host "Options:"
|
|
469
|
+
Write-Host " -OutDir <dir> Local directory (default: from URL)"
|
|
470
|
+
Write-Host " -Ref <branch/tag/sha> Default: remote HEAD"
|
|
471
|
+
Write-Host " -BatchSize <N> Files per batch, default 32"
|
|
472
|
+
Write-Host " -MaxRetries <N> Retries per failed batch/file, default 8"
|
|
473
|
+
Write-Host " -Include a,b Only download matching paths"
|
|
474
|
+
Write-Host " -Exclude a,b Skip matching paths"
|
|
475
|
+
Write-Host " -Depth <N> Optional shallow clone depth"
|
|
476
|
+
Write-Host " -Verify Hash-check local files on resume"
|
|
477
|
+
Write-Host " -ForceRefetch Always fetch the target ref"
|
|
478
|
+
Write-Host " -DryRun List files, do not checkout blobs"
|
|
479
|
+
Write-Host " -Tui Force fullscreen TUI"
|
|
480
|
+
Write-Host " -NoTui Disable TUI (script/CI mode)"
|
|
481
|
+
Write-Host " -Language zh-CN|en-US User interface language"
|
|
482
|
+
Write-Host " -ResumeLast Resume the latest history entry"
|
|
483
|
+
Write-Host " -Help Show this help"
|
|
484
|
+
Write-Host ""
|
|
485
|
+
Write-Host "Interactive: run with no URL to open the TUI wizard."
|
|
486
|
+
Write-Host "Resume: run the same command again. State is in .git/partial-resume/"
|
|
487
|
+
Write-Host "Keys: Q stop P pause F failures ? help Ctrl+C twice to kill git"
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if ($Help) {
|
|
491
|
+
Show-Usage
|
|
492
|
+
exit 0
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function Get-RepoFolderName {
|
|
496
|
+
param([string]$Url)
|
|
497
|
+
$s = $Url.Trim().TrimEnd([char]47, [char]92)
|
|
498
|
+
if ($s.Length -ge 4 -and $s.EndsWith(".git", [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
499
|
+
$s = $s.Substring(0, $s.Length - 4)
|
|
500
|
+
}
|
|
501
|
+
$s = $s.Replace([char]92, [char]47)
|
|
502
|
+
$i = $s.LastIndexOf([char]47)
|
|
503
|
+
if ($i -ge 0) { $s = $s.Substring($i + 1) }
|
|
504
|
+
$colon = $s.LastIndexOf([char]58)
|
|
505
|
+
if ($colon -ge 0) { $s = $s.Substring($colon + 1) }
|
|
506
|
+
if ([string]::IsNullOrWhiteSpace($s)) { return "repo" }
|
|
507
|
+
return $s
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function Convert-ToFullPath {
|
|
511
|
+
param([string]$Path)
|
|
512
|
+
if ([System.IO.Path]::IsPathRooted($Path)) {
|
|
513
|
+
return [System.IO.Path]::GetFullPath($Path)
|
|
514
|
+
}
|
|
515
|
+
return [System.IO.Path]::GetFullPath((Join-Path (Get-Location).Path $Path))
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function Get-WorktreePath {
|
|
519
|
+
param([string]$Root, [string]$Rel)
|
|
520
|
+
$acc = $Root
|
|
521
|
+
foreach ($p in ($Rel -split "[\\/]")) {
|
|
522
|
+
if ([string]::IsNullOrEmpty($p) -or $p -eq ".") { continue }
|
|
523
|
+
$acc = Join-Path $acc $p
|
|
524
|
+
}
|
|
525
|
+
return $acc
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function Test-GitAvailable {
|
|
529
|
+
try {
|
|
530
|
+
$null = Get-Command git -ErrorAction Stop
|
|
531
|
+
} catch {
|
|
532
|
+
throw "未找到 git。请先安装 Git for Windows: https://git-scm.com/download/win"
|
|
533
|
+
}
|
|
534
|
+
$verText = (& git --version 2>$null | Out-String).Trim()
|
|
535
|
+
Write-Log "使用 $verText" "INFO"
|
|
536
|
+
if ($verText -match "git version (\d+)\.(\d+)") {
|
|
537
|
+
$major = [int]$Matches[1]
|
|
538
|
+
$minor = [int]$Matches[2]
|
|
539
|
+
if ($major -lt 2 -or ($major -eq 2 -and $minor -lt 19)) {
|
|
540
|
+
Write-Log "partial clone 需要 Git >= 2.19,当前: $verText 。将继续尝试,失败请升级 Git。" "WARN"
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function Get-GitExePath {
|
|
546
|
+
$cmd = Get-Command git -ErrorAction Stop
|
|
547
|
+
if ($cmd.Source) { return $cmd.Source }
|
|
548
|
+
if ($cmd.Path) { return $cmd.Path }
|
|
549
|
+
return "git"
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function Convert-ToGitArgumentString {
|
|
553
|
+
param([string[]]$GitArgs)
|
|
554
|
+
$quoted = New-Object System.Text.StringBuilder
|
|
555
|
+
foreach ($a in $GitArgs) {
|
|
556
|
+
if ($null -eq $a) { continue }
|
|
557
|
+
if ($quoted.Length -gt 0) { [void]$quoted.Append(" ") }
|
|
558
|
+
$needsQuote = ($a -match "\s") -or ($a -match '"') -or ($a.Length -eq 0)
|
|
559
|
+
if ($needsQuote) {
|
|
560
|
+
$escaped = $a.Replace([string][char]34, [string][char]92 + [string][char]34)
|
|
561
|
+
[void]$quoted.Append('"').Append($escaped).Append('"')
|
|
562
|
+
} else {
|
|
563
|
+
[void]$quoted.Append($a)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return $quoted.ToString()
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function Invoke-GitProcess {
|
|
570
|
+
param(
|
|
571
|
+
[Parameter(Mandatory = $true)][string[]]$GitArgs,
|
|
572
|
+
[string]$WorkDir,
|
|
573
|
+
[int]$TimeoutMs = 0,
|
|
574
|
+
[switch]$ExpectFail,
|
|
575
|
+
[switch]$InheritConsole,
|
|
576
|
+
[string]$Heartbeat
|
|
577
|
+
)
|
|
578
|
+
if (-not $script:GitExe) { $script:GitExe = Get-GitExePath }
|
|
579
|
+
|
|
580
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
581
|
+
$psi.FileName = $script:GitExe
|
|
582
|
+
$psi.UseShellExecute = $false
|
|
583
|
+
$psi.CreateNoWindow = -not $InheritConsole
|
|
584
|
+
$psi.RedirectStandardInput = $true
|
|
585
|
+
if ($InheritConsole) {
|
|
586
|
+
$psi.RedirectStandardOutput = $false
|
|
587
|
+
$psi.RedirectStandardError = $false
|
|
588
|
+
} else {
|
|
589
|
+
$psi.RedirectStandardOutput = $true
|
|
590
|
+
$psi.RedirectStandardError = $true
|
|
591
|
+
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
|
|
592
|
+
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
|
|
593
|
+
}
|
|
594
|
+
if ($WorkDir) { $psi.WorkingDirectory = $WorkDir }
|
|
595
|
+
$psi.Arguments = Convert-ToGitArgumentString -GitArgs $GitArgs
|
|
596
|
+
|
|
597
|
+
$proc = New-Object System.Diagnostics.Process
|
|
598
|
+
$proc.StartInfo = $psi
|
|
599
|
+
$stdout = ""
|
|
600
|
+
$stderr = ""
|
|
601
|
+
$script:GcrCurrentProc = $proc
|
|
602
|
+
try {
|
|
603
|
+
[void]$proc.Start()
|
|
604
|
+
$proc.StandardInput.Close()
|
|
605
|
+
$waitSlice = 80
|
|
606
|
+
if (-not (Test-GcrTuiActive)) { $waitSlice = 500 }
|
|
607
|
+
$waited = 0
|
|
608
|
+
$hbSec = 2
|
|
609
|
+
$useStream = (Test-GcrTuiActive) -and (-not $InheritConsole)
|
|
610
|
+
$stdoutTask = $null
|
|
611
|
+
$stderrTask = $null
|
|
612
|
+
$outBuf = $null
|
|
613
|
+
$errBuf = $null
|
|
614
|
+
$outCarry = $null
|
|
615
|
+
$errCarry = $null
|
|
616
|
+
$outRead = $null
|
|
617
|
+
$errRead = $null
|
|
618
|
+
$stdoutSb = New-Object System.Text.StringBuilder
|
|
619
|
+
$stderrSb = New-Object System.Text.StringBuilder
|
|
620
|
+
if ($useStream) {
|
|
621
|
+
$outBuf = New-Object byte[] 4096
|
|
622
|
+
$errBuf = New-Object byte[] 4096
|
|
623
|
+
$outCarry = New-Object System.Text.StringBuilder
|
|
624
|
+
$errCarry = New-Object System.Text.StringBuilder
|
|
625
|
+
$outRead = $proc.StandardOutput.BaseStream.ReadAsync($outBuf, 0, $outBuf.Length)
|
|
626
|
+
$errRead = $proc.StandardError.BaseStream.ReadAsync($errBuf, 0, $errBuf.Length)
|
|
627
|
+
} elseif (-not $InheritConsole) {
|
|
628
|
+
$stdoutTask = $proc.StandardOutput.ReadToEndAsync()
|
|
629
|
+
$stderrTask = $proc.StandardError.ReadToEndAsync()
|
|
630
|
+
}
|
|
631
|
+
while (-not $proc.HasExited) {
|
|
632
|
+
if ($useStream) {
|
|
633
|
+
if ($null -ne $outRead -and $outRead.IsCompleted) {
|
|
634
|
+
$n = 0
|
|
635
|
+
try { $n = [int]$outRead.Result } catch { $n = 0 }
|
|
636
|
+
if ($n -gt 0) {
|
|
637
|
+
$chunk = [System.Text.Encoding]::UTF8.GetString($outBuf, 0, $n)
|
|
638
|
+
[void]$stdoutSb.Append($chunk)
|
|
639
|
+
$outRead = $proc.StandardOutput.BaseStream.ReadAsync($outBuf, 0, $outBuf.Length)
|
|
640
|
+
} else { $outRead = $null }
|
|
641
|
+
}
|
|
642
|
+
if ($null -ne $errRead -and $errRead.IsCompleted) {
|
|
643
|
+
$n = 0
|
|
644
|
+
try { $n = [int]$errRead.Result } catch { $n = 0 }
|
|
645
|
+
if ($n -gt 0) {
|
|
646
|
+
[void]$stderrSb.Append([System.Text.Encoding]::UTF8.GetString($errBuf, 0, $n))
|
|
647
|
+
Receive-GcrGitBytes -Buffer $errBuf -Count $n -Carry $errCarry -IsStdErr
|
|
648
|
+
$errRead = $proc.StandardError.BaseStream.ReadAsync($errBuf, 0, $errBuf.Length)
|
|
649
|
+
} else { $errRead = $null }
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (-not $proc.WaitForExit($waitSlice)) {
|
|
653
|
+
$waited += $waitSlice
|
|
654
|
+
if ($TimeoutMs -gt 0 -and $waited -ge $TimeoutMs) {
|
|
655
|
+
try { $proc.Kill() } catch { }
|
|
656
|
+
throw "git 命令超时 (" + $TimeoutMs + "ms): git " + $psi.Arguments
|
|
657
|
+
}
|
|
658
|
+
if ($Heartbeat -and ($waited % ($hbSec * 1000) -lt $waitSlice)) {
|
|
659
|
+
$sec = [int]($waited / 1000)
|
|
660
|
+
if (Test-GcrTuiActive) {
|
|
661
|
+
Set-GcrTuiPhase -Detail ($Heartbeat + " ... " + $sec + "s")
|
|
662
|
+
} else {
|
|
663
|
+
Write-Host ("`r[WAIT] " + $Heartbeat + " ... " + $sec + "s ") -NoNewline
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (Test-GcrTuiActive) {
|
|
668
|
+
Invoke-GcrTuiTick
|
|
669
|
+
if (Test-GcrTuiForceQuit) {
|
|
670
|
+
try { $proc.Kill() } catch { }
|
|
671
|
+
$script:GcrUserStop = $true
|
|
672
|
+
throw "已由用户停止。"
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if ($Heartbeat -and -not (Test-GcrTuiActive)) { Write-Host "" }
|
|
677
|
+
if ($useStream) {
|
|
678
|
+
if ($null -ne $outRead) {
|
|
679
|
+
try {
|
|
680
|
+
$n = [int]$outRead.Result
|
|
681
|
+
if ($n -gt 0) { [void]$stdoutSb.Append([System.Text.Encoding]::UTF8.GetString($outBuf, 0, $n)) }
|
|
682
|
+
} catch { }
|
|
683
|
+
}
|
|
684
|
+
if ($null -ne $errRead) {
|
|
685
|
+
try {
|
|
686
|
+
$n = [int]$errRead.Result
|
|
687
|
+
if ($n -gt 0) {
|
|
688
|
+
[void]$stderrSb.Append([System.Text.Encoding]::UTF8.GetString($errBuf, 0, $n))
|
|
689
|
+
Receive-GcrGitBytes -Buffer $errBuf -Count $n -Carry $errCarry -IsStdErr
|
|
690
|
+
}
|
|
691
|
+
} catch { }
|
|
692
|
+
}
|
|
693
|
+
if ($errCarry -and $errCarry.Length -gt 0) { Add-GcrGitOutput -Text $errCarry.ToString() }
|
|
694
|
+
$stdout = $stdoutSb.ToString()
|
|
695
|
+
$stderr = $stderrSb.ToString()
|
|
696
|
+
} elseif (-not $InheritConsole) {
|
|
697
|
+
[void]$stdoutTask.Wait()
|
|
698
|
+
[void]$stderrTask.Wait()
|
|
699
|
+
$stdout = $stdoutTask.Result
|
|
700
|
+
$stderr = $stderrTask.Result
|
|
701
|
+
}
|
|
702
|
+
if ($script:GcrUserStop) { throw "已由用户停止。" }
|
|
703
|
+
$code = $proc.ExitCode
|
|
704
|
+
} finally {
|
|
705
|
+
$script:GcrCurrentProc = $null
|
|
706
|
+
$proc.Dispose()
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if ($null -eq $stdout) { $stdout = "" }
|
|
710
|
+
if ($null -eq $stderr) { $stderr = "" }
|
|
711
|
+
|
|
712
|
+
if (-not $ExpectFail -and $code -ne 0) {
|
|
713
|
+
$err = $stderr
|
|
714
|
+
if ([string]::IsNullOrWhiteSpace($err)) { $err = $stdout }
|
|
715
|
+
$msg = "git 失败 (exit $code): git " + $psi.Arguments
|
|
716
|
+
if (-not [string]::IsNullOrWhiteSpace($err)) { $msg = $msg + [Environment]::NewLine + $err.Trim() }
|
|
717
|
+
throw $msg
|
|
718
|
+
}
|
|
719
|
+
return [pscustomobject]@{
|
|
720
|
+
ExitCode = $code
|
|
721
|
+
StdOut = $stdout
|
|
722
|
+
StdErr = $stderr
|
|
723
|
+
Args = $GitArgs
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function Invoke-Git {
|
|
728
|
+
param(
|
|
729
|
+
[Parameter(Mandatory = $true)][string[]]$GitArgs,
|
|
730
|
+
[string]$WorkDir
|
|
731
|
+
)
|
|
732
|
+
$r = Invoke-GitProcess -GitArgs $GitArgs -WorkDir $WorkDir
|
|
733
|
+
return $r.StdOut
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function Clear-StaleIndexLock {
|
|
737
|
+
param([string]$RepoRoot)
|
|
738
|
+
if (-not $RepoRoot) { return }
|
|
739
|
+
$lock = Join-Path $RepoRoot ".git\index.lock"
|
|
740
|
+
if (Test-Path -LiteralPath $lock) {
|
|
741
|
+
$age = (Get-Date) - (Get-Item -LiteralPath $lock).LastWriteTime
|
|
742
|
+
if ($age.TotalMinutes -ge 2) {
|
|
743
|
+
Write-Log ("删除过期 index.lock (" + [int]$age.TotalMinutes + " 分钟)") "WARN"
|
|
744
|
+
Remove-Item -LiteralPath $lock -Force -ErrorAction SilentlyContinue
|
|
745
|
+
} else {
|
|
746
|
+
Write-Log ("检测到 index.lock (" + [int]$age.TotalSeconds + "s)。若确认没有其它 git 进程,请手动删除: " + $lock) "WARN"
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function Invoke-GitRetry {
|
|
752
|
+
param(
|
|
753
|
+
[Parameter(Mandatory = $true)][string[]]$GitArgs,
|
|
754
|
+
[string]$WorkDir,
|
|
755
|
+
[string]$What,
|
|
756
|
+
[int]$TimeoutMs = 0,
|
|
757
|
+
[switch]$InheritConsole,
|
|
758
|
+
[string]$Heartbeat,
|
|
759
|
+
[int]$Retries = -1
|
|
760
|
+
)
|
|
761
|
+
$attempt = 0
|
|
762
|
+
$delay = [Math]::Max(0, $RetryDelaySeconds)
|
|
763
|
+
$lastError = $null
|
|
764
|
+
$limit = $MaxRetries
|
|
765
|
+
if ($Retries -ge 0) { $limit = $Retries }
|
|
766
|
+
$limit = [Math]::Max(1, $limit)
|
|
767
|
+
while ($attempt -lt $limit) {
|
|
768
|
+
$attempt++
|
|
769
|
+
try {
|
|
770
|
+
$r = Invoke-GitProcess -GitArgs $GitArgs -WorkDir $WorkDir -TimeoutMs $TimeoutMs -ExpectFail -InheritConsole:$InheritConsole -Heartbeat $Heartbeat
|
|
771
|
+
if ($script:GcrUserStop) { throw "已由用户停止。" }
|
|
772
|
+
if ($r.ExitCode -eq 0) { return $r }
|
|
773
|
+
$lastError = "exit " + $r.ExitCode
|
|
774
|
+
$tail = $r.StdErr
|
|
775
|
+
if ([string]::IsNullOrWhiteSpace($tail)) { $tail = $r.StdOut }
|
|
776
|
+
if (-not [string]::IsNullOrWhiteSpace($tail)) { $lastError = $lastError + " : " + $tail.Trim() }
|
|
777
|
+
} catch {
|
|
778
|
+
if ($script:GcrUserStop) { throw }
|
|
779
|
+
$lastError = $_.Exception.Message
|
|
780
|
+
}
|
|
781
|
+
if ($attempt -ge $limit) { break }
|
|
782
|
+
Write-Log ($What + " 失败 (第 " + $attempt + "/" + $limit + " 次): " + $lastError + " ;" + $delay + "s 后重试") "WARN"
|
|
783
|
+
Start-Sleep -Seconds $delay
|
|
784
|
+
$delay = [Math]::Min(60, [Math]::Max(1, $delay * 2))
|
|
785
|
+
Clear-StaleIndexLock -RepoRoot $WorkDir
|
|
786
|
+
}
|
|
787
|
+
throw ($What + " 在 " + $limit + " 次重试后仍失败: " + $lastError)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function Save-TextFile {
|
|
791
|
+
param([string]$Path, [string]$Content)
|
|
792
|
+
$dir = Split-Path -Parent $Path
|
|
793
|
+
if ($dir -and -not (Test-Path -LiteralPath $dir)) {
|
|
794
|
+
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
795
|
+
}
|
|
796
|
+
[System.IO.File]::WriteAllText($Path, $Content, $script:Utf8NoBom)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function Read-Meta {
|
|
800
|
+
param([string]$MetaPath)
|
|
801
|
+
$map = @{}
|
|
802
|
+
if (-not (Test-Path -LiteralPath $MetaPath)) { return $map }
|
|
803
|
+
foreach ($line in [System.IO.File]::ReadAllLines($MetaPath, $script:Utf8NoBom)) {
|
|
804
|
+
$eq = $line.IndexOf("=")
|
|
805
|
+
if ($eq -lt 1) { continue }
|
|
806
|
+
if ($line.StartsWith("#")) { continue }
|
|
807
|
+
$k = $line.Substring(0, $eq).Trim()
|
|
808
|
+
$v = $line.Substring($eq + 1)
|
|
809
|
+
$map[$k] = $v
|
|
810
|
+
}
|
|
811
|
+
return $map
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function Write-Meta {
|
|
815
|
+
param([string]$MetaPath, [hashtable]$Map)
|
|
816
|
+
$sb = New-Object System.Text.StringBuilder
|
|
817
|
+
[void]$sb.AppendLine("# git-clone-resume state")
|
|
818
|
+
foreach ($k in ($Map.Keys | Sort-Object)) {
|
|
819
|
+
[void]$sb.AppendLine($k + "=" + $Map[$k])
|
|
820
|
+
}
|
|
821
|
+
Save-TextFile -Path $MetaPath -Content $sb.ToString()
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function Test-WildcardMatch {
|
|
825
|
+
param([string]$Path, [string[]]$Patterns)
|
|
826
|
+
if (-not $Patterns -or @($Patterns).Count -eq 0) { return $false }
|
|
827
|
+
$norm = $Path.Replace("\", "/")
|
|
828
|
+
foreach ($p in @($Patterns)) {
|
|
829
|
+
if ([string]::IsNullOrWhiteSpace($p)) { continue }
|
|
830
|
+
$pat = $p.Trim().Replace("\", "/")
|
|
831
|
+
if ($norm -like $pat) { return $true }
|
|
832
|
+
$prefix = $pat.TrimEnd("/")
|
|
833
|
+
if ($prefix -and $norm -like ($prefix + "/*")) { return $true }
|
|
834
|
+
}
|
|
835
|
+
return $false
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function Initialize-PartialRepo {
|
|
839
|
+
param([string]$RepoRoot, [string]$Url)
|
|
840
|
+
$gitDir = Join-Path $RepoRoot ".git"
|
|
841
|
+
if (-not (Test-Path -LiteralPath $gitDir)) {
|
|
842
|
+
if (-not (Test-Path -LiteralPath $RepoRoot)) {
|
|
843
|
+
New-Item -ItemType Directory -Path $RepoRoot -Force | Out-Null
|
|
844
|
+
}
|
|
845
|
+
Write-Log "git init $RepoRoot" "STEP"
|
|
846
|
+
Invoke-GitRetry -What "git init" -WorkDir $RepoRoot -GitArgs @("init") | Out-Null
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("config", "core.longpaths", "true") | Out-Null
|
|
850
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("config", "core.quotepath", "false") | Out-Null
|
|
851
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("config", "core.precomposeunicode", "true") | Out-Null
|
|
852
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("config", "i18n.logOutputEncoding", "utf-8") | Out-Null
|
|
853
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("config", "gc.auto", "0") | Out-Null
|
|
854
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("config", "http.version", "HTTP/1.1") | Out-Null
|
|
855
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("config", "http.postBuffer", "524288000") | Out-Null
|
|
856
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("config", "http.lowSpeedLimit", "1024") | Out-Null
|
|
857
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("config", "http.lowSpeedTime", "60") | Out-Null
|
|
858
|
+
|
|
859
|
+
$existingRemote = ""
|
|
860
|
+
$remoteProbe = Invoke-GitProcess -WorkDir $RepoRoot -ExpectFail -GitArgs @("remote", "get-url", "origin")
|
|
861
|
+
if ($remoteProbe.ExitCode -eq 0) { $existingRemote = $remoteProbe.StdOut.Trim() }
|
|
862
|
+
|
|
863
|
+
if ([string]::IsNullOrWhiteSpace($existingRemote)) {
|
|
864
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("remote", "add", "origin", $Url) | Out-Null
|
|
865
|
+
} else {
|
|
866
|
+
$a = $existingRemote.Trim().TrimEnd("/")
|
|
867
|
+
$b = $Url.Trim().TrimEnd("/")
|
|
868
|
+
if ($a -ne $b -and ($a + ".git") -ne $b -and $a -ne ($b + ".git")) {
|
|
869
|
+
Write-Log "已有 origin=$existingRemote ,与本次 URL 不同,改为 $Url" "WARN"
|
|
870
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("remote", "set-url", "origin", $Url) | Out-Null
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("config", "remote.origin.promisor", "true") | Out-Null
|
|
875
|
+
Invoke-Git -WorkDir $RepoRoot -GitArgs @("config", "remote.origin.partialclonefilter", "blob:none") | Out-Null
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function Set-DetachedHead {
|
|
879
|
+
param([string]$RepoRoot, [string]$Sha)
|
|
880
|
+
$headFile = Join-Path $RepoRoot ".git\HEAD"
|
|
881
|
+
Save-TextFile -Path $headFile -Content ($Sha + [Environment]::NewLine)
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function Get-TreeEntries {
|
|
885
|
+
param([string]$RepoRoot, [string]$Sha)
|
|
886
|
+
# Write git stdout to a file as raw bytes. Capturing via StreamReader in PS 5.1
|
|
887
|
+
# can collapse the whole tree into one fake path.
|
|
888
|
+
# Do NOT use -l (blob:none would fetch every blob for sizes) or -z (NUL truncation).
|
|
889
|
+
$gitDir = Join-Path $RepoRoot ".git"
|
|
890
|
+
$stateDir = Join-Path $gitDir $script:StateDirName
|
|
891
|
+
if (-not (Test-Path -LiteralPath $stateDir)) {
|
|
892
|
+
New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
|
|
893
|
+
}
|
|
894
|
+
$outFile = Join-Path $stateDir "ls-tree.raw"
|
|
895
|
+
$errFile = Join-Path $stateDir "ls-tree.err"
|
|
896
|
+
if (Test-Path -LiteralPath $outFile) { Remove-Item -LiteralPath $outFile -Force }
|
|
897
|
+
if (Test-Path -LiteralPath $errFile) { Remove-Item -LiteralPath $errFile -Force }
|
|
898
|
+
if (-not $script:GitExe) { $script:GitExe = Get-GitExePath }
|
|
899
|
+
|
|
900
|
+
$arg = "-c core.quotepath=false ls-tree -r " + $Sha
|
|
901
|
+
$p = Start-Process -FilePath $script:GitExe -WorkingDirectory $RepoRoot `
|
|
902
|
+
-ArgumentList $arg `
|
|
903
|
+
-RedirectStandardOutput $outFile -RedirectStandardError $errFile `
|
|
904
|
+
-Wait -NoNewWindow -PassThru
|
|
905
|
+
if ($p.ExitCode -ne 0) {
|
|
906
|
+
$err = ""
|
|
907
|
+
if (Test-Path -LiteralPath $errFile) {
|
|
908
|
+
$err = [System.IO.File]::ReadAllText($errFile, $script:Utf8NoBom)
|
|
909
|
+
}
|
|
910
|
+
throw ("ls-tree failed (exit " + $p.ExitCode + "): " + $err.Trim())
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
$entries = New-Object System.Collections.Generic.List[object]
|
|
914
|
+
if (-not (Test-Path -LiteralPath $outFile)) { return ,$entries }
|
|
915
|
+
$bytes = [System.IO.File]::ReadAllBytes($outFile)
|
|
916
|
+
if ($null -eq $bytes -or $bytes.Length -eq 0) { return ,$entries }
|
|
917
|
+
$raw = [System.Text.Encoding]::UTF8.GetString($bytes)
|
|
918
|
+
|
|
919
|
+
foreach ($rec in $raw.Split(@([char]10), [System.StringSplitOptions]::None)) {
|
|
920
|
+
$rec = $rec.TrimEnd([char]13, [char]0)
|
|
921
|
+
if ([string]::IsNullOrWhiteSpace($rec)) { continue }
|
|
922
|
+
$tab = $rec.IndexOf([char]9)
|
|
923
|
+
if ($tab -lt 0) { continue }
|
|
924
|
+
$metaBits = $rec.Substring(0, $tab)
|
|
925
|
+
$path = $rec.Substring($tab + 1).TrimEnd()
|
|
926
|
+
$bits = @($metaBits -split "\s+", 3)
|
|
927
|
+
if ($bits.Count -lt 3) { continue }
|
|
928
|
+
if ([string]::IsNullOrWhiteSpace($path)) { continue }
|
|
929
|
+
[void]$entries.Add([pscustomobject]@{
|
|
930
|
+
Mode = $bits[0]
|
|
931
|
+
Type = $bits[1]
|
|
932
|
+
Blob = $bits[2]
|
|
933
|
+
Size = 0L
|
|
934
|
+
Path = $path
|
|
935
|
+
})
|
|
936
|
+
}
|
|
937
|
+
Write-Log ("ls-tree bytes=" + $bytes.Length + " files=" + $entries.Count) "INFO"
|
|
938
|
+
return ,$entries
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function Get-LocalBlobHash {
|
|
942
|
+
param([string]$RepoRoot, [string]$RelPath)
|
|
943
|
+
$full = Get-WorktreePath -Root $RepoRoot -Rel $RelPath
|
|
944
|
+
if (-not (Test-Path -LiteralPath $full)) { return $null }
|
|
945
|
+
$r = Invoke-GitProcess -WorkDir $RepoRoot -ExpectFail -GitArgs @("hash-object", "--path", $RelPath, "--", $RelPath)
|
|
946
|
+
if ($r.ExitCode -eq 0) { return $r.StdOut.Trim() }
|
|
947
|
+
return $null
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function Test-FileComplete {
|
|
951
|
+
param(
|
|
952
|
+
[string]$RepoRoot,
|
|
953
|
+
$Entry,
|
|
954
|
+
[switch]$HashVerify
|
|
955
|
+
)
|
|
956
|
+
$full = Get-WorktreePath -Root $RepoRoot -Rel $Entry.Path
|
|
957
|
+
if (-not (Test-Path -LiteralPath $full)) { return $false }
|
|
958
|
+
$item = Get-Item -LiteralPath $full -Force -ErrorAction SilentlyContinue
|
|
959
|
+
if (-not $item -or $item.PSIsContainer) { return $false }
|
|
960
|
+
|
|
961
|
+
if ($HashVerify) {
|
|
962
|
+
$h = Get-LocalBlobHash -RepoRoot $RepoRoot -RelPath $Entry.Path
|
|
963
|
+
return ($h -eq $Entry.Blob)
|
|
964
|
+
}
|
|
965
|
+
# Do not compare raw byte length to ls-tree size: core.autocrlf on Windows
|
|
966
|
+
# makes working-tree files larger than the blob.
|
|
967
|
+
return $true
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function Split-Batches {
|
|
971
|
+
param(
|
|
972
|
+
[object[]]$Items,
|
|
973
|
+
[int]$MaxCount,
|
|
974
|
+
[int]$MaxChars
|
|
975
|
+
)
|
|
976
|
+
$batches = New-Object System.Collections.Generic.List[object]
|
|
977
|
+
$cur = New-Object System.Collections.Generic.List[object]
|
|
978
|
+
$chars = 0
|
|
979
|
+
foreach ($it in $Items) {
|
|
980
|
+
$add = $it.Path.Length + 3
|
|
981
|
+
if ($cur.Count -gt 0 -and (($cur.Count -ge $MaxCount) -or ($chars + $add -gt $MaxChars))) {
|
|
982
|
+
[void]$batches.Add((New-BatchCopy -Items $cur))
|
|
983
|
+
$cur = New-Object System.Collections.Generic.List[object]
|
|
984
|
+
$chars = 0
|
|
985
|
+
}
|
|
986
|
+
[void]$cur.Add($it)
|
|
987
|
+
$chars += $add
|
|
988
|
+
}
|
|
989
|
+
if ($cur.Count -gt 0) {
|
|
990
|
+
[void]$batches.Add((New-BatchCopy -Items $cur))
|
|
991
|
+
}
|
|
992
|
+
return ,$batches
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function New-BatchCopy {
|
|
996
|
+
param($Items)
|
|
997
|
+
$copy = New-Object System.Collections.Generic.List[object]
|
|
998
|
+
foreach ($it in $Items) { [void]$copy.Add($it) }
|
|
999
|
+
return ,$copy
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function Format-Bytes {
|
|
1003
|
+
param([int64]$n)
|
|
1004
|
+
if ($n -lt 1024) { return "$n B" }
|
|
1005
|
+
if ($n -lt 1MB) { return ("{0:N1} KB" -f ($n / 1KB)) }
|
|
1006
|
+
if ($n -lt 1GB) { return ("{0:N1} MB" -f ($n / 1MB)) }
|
|
1007
|
+
return ("{0:N2} GB" -f ($n / 1GB))
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function Add-DonePaths {
|
|
1011
|
+
param([string]$DonePath, [string[]]$Paths)
|
|
1012
|
+
if (-not $Paths -or @($Paths).Count -eq 0) { return }
|
|
1013
|
+
$text = (@($Paths) -join [Environment]::NewLine) + [Environment]::NewLine
|
|
1014
|
+
[System.IO.File]::AppendAllText($DonePath, $text, $script:Utf8NoBom)
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function Add-FailedPath {
|
|
1018
|
+
param([string]$FailedPath, [string]$RelPath, [string]$Reason)
|
|
1019
|
+
$safe = $Reason -replace "[\r\n]+", " "
|
|
1020
|
+
$line = (Get-Date -Format o) + [char]9 + $RelPath + [char]9 + $safe + [Environment]::NewLine
|
|
1021
|
+
[System.IO.File]::AppendAllText($FailedPath, $line, $script:Utf8NoBom)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function Load-DoneSet {
|
|
1025
|
+
param([string]$DonePath)
|
|
1026
|
+
$set = New-Object "System.Collections.Generic.HashSet[string]" ([System.StringComparer]::Ordinal)
|
|
1027
|
+
if (Test-Path -LiteralPath $DonePath) {
|
|
1028
|
+
foreach ($line in [System.IO.File]::ReadAllLines($DonePath, $script:Utf8NoBom)) {
|
|
1029
|
+
$p = $line.Trim()
|
|
1030
|
+
if ($p) { [void]$set.Add($p) }
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
# unary comma: stop PowerShell from enumerating the HashSet (empty => $null)
|
|
1034
|
+
return ,$set
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function Invoke-CheckoutBatch {
|
|
1038
|
+
param(
|
|
1039
|
+
[string]$RepoRoot,
|
|
1040
|
+
[string]$Sha,
|
|
1041
|
+
$Batch
|
|
1042
|
+
)
|
|
1043
|
+
$gitArgsList = New-Object System.Collections.Generic.List[string]
|
|
1044
|
+
foreach ($x in @("-c", "core.quotepath=false", "-c", "core.longpaths=true", "-c", "advice.detachedHead=false", "checkout", "--progress", $Sha, "--")) {
|
|
1045
|
+
[void]$gitArgsList.Add([string]$x)
|
|
1046
|
+
}
|
|
1047
|
+
$n = 0
|
|
1048
|
+
foreach ($e in $Batch) {
|
|
1049
|
+
[void]$gitArgsList.Add([string]$e.Path)
|
|
1050
|
+
$n++
|
|
1051
|
+
}
|
|
1052
|
+
if ($n -le 0) { return }
|
|
1053
|
+
Ensure-ParentDirectories -RepoRoot $RepoRoot -Batch $Batch
|
|
1054
|
+
$first = [string]$Batch[0].Path
|
|
1055
|
+
$hb = "downloading " + $n + " file(s), e.g. " + $first
|
|
1056
|
+
# Multi-file checkout: try once. Retrying the same 64-file batch on Windows
|
|
1057
|
+
# wastes minutes (sha1 missing + cannot create directory) and can desync the index.
|
|
1058
|
+
$tries = 1
|
|
1059
|
+
if ($n -eq 1) { $tries = $MaxRetries }
|
|
1060
|
+
Invoke-GitRetry -What ("checkout " + $n + " files") -WorkDir $RepoRoot -Heartbeat $hb -Retries $tries -GitArgs $gitArgsList.ToArray() | Out-Null
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function Ensure-ParentDirectories {
|
|
1064
|
+
param([string]$RepoRoot, $Batch)
|
|
1065
|
+
$seen = New-Object "System.Collections.Generic.HashSet[string]" ([System.StringComparer]::OrdinalIgnoreCase)
|
|
1066
|
+
foreach ($e in $Batch) {
|
|
1067
|
+
$rel = [string]$e.Path
|
|
1068
|
+
$slash = $rel.LastIndexOf([char]47)
|
|
1069
|
+
if ($slash -lt 1) { continue }
|
|
1070
|
+
$parentRel = $rel.Substring(0, $slash)
|
|
1071
|
+
if (-not $seen.Add($parentRel)) { continue }
|
|
1072
|
+
$full = Get-WorktreePath -Root $RepoRoot -Rel $parentRel
|
|
1073
|
+
if (Test-Path -LiteralPath $full -PathType Leaf) {
|
|
1074
|
+
Write-Log ("parent path is a file, removing so a directory can be created: " + $parentRel) "WARN"
|
|
1075
|
+
Remove-Item -LiteralPath $full -Force -ErrorAction SilentlyContinue
|
|
1076
|
+
}
|
|
1077
|
+
if (-not (Test-Path -LiteralPath $full)) {
|
|
1078
|
+
New-Item -ItemType Directory -Path $full -Force | Out-Null
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function Confirm-BatchFiles {
|
|
1084
|
+
param(
|
|
1085
|
+
[string]$RepoRoot,
|
|
1086
|
+
$Batch,
|
|
1087
|
+
[switch]$HashVerify
|
|
1088
|
+
)
|
|
1089
|
+
$ok = New-Object System.Collections.Generic.List[object]
|
|
1090
|
+
$bad = New-Object System.Collections.Generic.List[object]
|
|
1091
|
+
foreach ($e in $Batch) {
|
|
1092
|
+
if (Test-FileComplete -RepoRoot $RepoRoot -Entry $e -HashVerify:$HashVerify) {
|
|
1093
|
+
[void]$ok.Add($e)
|
|
1094
|
+
} else {
|
|
1095
|
+
[void]$bad.Add($e)
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return @{ Ok = $ok; Bad = $bad }
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function Repair-GitIndex {
|
|
1102
|
+
param([string]$RepoRoot, [string]$Sha)
|
|
1103
|
+
# Failed multi-path checkout on Windows can drop index entries while leaving
|
|
1104
|
+
# the files on disk (status: D + ??). Re-add existing files; re-checkout missing ones.
|
|
1105
|
+
$porcelain = Invoke-GitProcess -WorkDir $RepoRoot -ExpectFail -GitArgs @(
|
|
1106
|
+
"-c", "core.quotepath=false", "status", "--porcelain", "-uall"
|
|
1107
|
+
)
|
|
1108
|
+
if ($porcelain.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($porcelain.StdOut)) { return }
|
|
1109
|
+
$add = New-Object System.Collections.Generic.List[string]
|
|
1110
|
+
$needCheckout = New-Object System.Collections.Generic.List[string]
|
|
1111
|
+
foreach ($line in ($porcelain.StdOut -split [char]10)) {
|
|
1112
|
+
$line = $line.TrimEnd([char]13)
|
|
1113
|
+
if ($line.Length -lt 4) { continue }
|
|
1114
|
+
$code = $line.Substring(0, 2)
|
|
1115
|
+
$path = $line.Substring(3).Trim()
|
|
1116
|
+
if ($path.StartsWith(".git/")) { continue }
|
|
1117
|
+
$full = Get-WorktreePath -Root $RepoRoot -Rel $path
|
|
1118
|
+
$exists = Test-Path -LiteralPath $full -PathType Leaf
|
|
1119
|
+
if ($code -eq "D " -or $code -eq " D" -or $code -eq "??") {
|
|
1120
|
+
if ($exists) { [void]$add.Add($path) } else { [void]$needCheckout.Add($path) }
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if ($add.Count -gt 0) {
|
|
1124
|
+
Write-Log ("repair index: git add " + $add.Count + " files that exist on disk") "WARN"
|
|
1125
|
+
$args = New-Object System.Collections.Generic.List[string]
|
|
1126
|
+
foreach ($x in @("-c", "core.quotepath=false", "add", "-f", "--")) { [void]$args.Add($x) }
|
|
1127
|
+
foreach ($p in $add) { [void]$args.Add($p) }
|
|
1128
|
+
Invoke-GitProcess -WorkDir $RepoRoot -ExpectFail -GitArgs $args.ToArray() | Out-Null
|
|
1129
|
+
}
|
|
1130
|
+
if ($needCheckout.Count -gt 0) {
|
|
1131
|
+
Write-Log ("repair index: re-checkout " + $needCheckout.Count + " missing files") "WARN"
|
|
1132
|
+
$args = New-Object System.Collections.Generic.List[string]
|
|
1133
|
+
foreach ($x in @("-c", "core.quotepath=false", "checkout", $Sha, "--")) { [void]$args.Add($x) }
|
|
1134
|
+
foreach ($p in $needCheckout) { [void]$args.Add($p) }
|
|
1135
|
+
Invoke-GitProcess -WorkDir $RepoRoot -ExpectFail -GitArgs $args.ToArray() | Out-Null
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function Format-Eta {
|
|
1140
|
+
param([TimeSpan]$Elapsed, [int]$DoneThisRun, [int]$Remain)
|
|
1141
|
+
if ($Elapsed.TotalSeconds -lt 1 -or $DoneThisRun -le 0 -or $Remain -le 0) { return "--:--:--" }
|
|
1142
|
+
$rate = $DoneThisRun / $Elapsed.TotalSeconds
|
|
1143
|
+
if ($rate -le 0) { return "--:--:--" }
|
|
1144
|
+
$sec = [Math]::Min(864000, $Remain / $rate)
|
|
1145
|
+
$ts = [TimeSpan]::FromSeconds($sec)
|
|
1146
|
+
return ("{0:00}:{1:00}:{2:00}" -f [int]$ts.TotalHours, $ts.Minutes, $ts.Seconds)
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function Get-WorktreeBytes {
|
|
1150
|
+
param([string]$RepoRoot, [string]$RelPath)
|
|
1151
|
+
$full = Get-WorktreePath -Root $RepoRoot -Rel $RelPath
|
|
1152
|
+
if (-not (Test-Path -LiteralPath $full)) { return 0L }
|
|
1153
|
+
$item = Get-Item -LiteralPath $full -Force -ErrorAction SilentlyContinue
|
|
1154
|
+
if (-not $item -or $item.PSIsContainer) { return 0L }
|
|
1155
|
+
return [int64]$item.Length
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function Write-DownloadProgress {
|
|
1159
|
+
param(
|
|
1160
|
+
[int]$OkCount,
|
|
1161
|
+
[int]$TotalCount,
|
|
1162
|
+
[int]$FailCount,
|
|
1163
|
+
[int64]$DoneBytes,
|
|
1164
|
+
[TimeSpan]$Elapsed,
|
|
1165
|
+
[int]$DoneThisRun,
|
|
1166
|
+
[string]$CurrentFile,
|
|
1167
|
+
[int]$BarWidth = 28
|
|
1168
|
+
)
|
|
1169
|
+
$pct = 0.0
|
|
1170
|
+
if ($TotalCount -gt 0) { $pct = 100.0 * $OkCount / $TotalCount }
|
|
1171
|
+
$filled = 0
|
|
1172
|
+
if ($TotalCount -gt 0) {
|
|
1173
|
+
$filled = [int][Math]::Round($BarWidth * $OkCount / $TotalCount)
|
|
1174
|
+
if ($filled -gt $BarWidth) { $filled = $BarWidth }
|
|
1175
|
+
}
|
|
1176
|
+
$empty = $BarWidth - $filled
|
|
1177
|
+
$bar = ("#" * $filled) + ("-" * $empty)
|
|
1178
|
+
$remain = $TotalCount - $OkCount - $FailCount
|
|
1179
|
+
$eta = Format-Eta -Elapsed $Elapsed -DoneThisRun $DoneThisRun -Remain $remain
|
|
1180
|
+
$rate = 0.0
|
|
1181
|
+
if ($Elapsed.TotalSeconds -gt 0.5 -and $DoneThisRun -gt 0) {
|
|
1182
|
+
$rate = $DoneThisRun / $Elapsed.TotalSeconds
|
|
1183
|
+
}
|
|
1184
|
+
$name = [string]$CurrentFile
|
|
1185
|
+
if ($name.Length -gt 48) { $name = "..." + $name.Substring($name.Length - 45) }
|
|
1186
|
+
$line = ("[{0}] {1,5:N1}% {2}/{3} fail {4} {5} {6:N1} files/s ETA {7} {8}" -f @(
|
|
1187
|
+
$bar, $pct, $OkCount, $TotalCount, $FailCount, (Format-Bytes $DoneBytes), $rate, $eta, $name
|
|
1188
|
+
))
|
|
1189
|
+
$width = 120
|
|
1190
|
+
try {
|
|
1191
|
+
$w = [int]$Host.UI.RawUI.WindowSize.Width
|
|
1192
|
+
if ($w -gt 20) { $width = $w - 1 }
|
|
1193
|
+
} catch { }
|
|
1194
|
+
$out = $line
|
|
1195
|
+
if ($out.Length -lt $width) { $out = $out.PadRight($width) }
|
|
1196
|
+
elseif ($out.Length -gt $width) { $out = $out.Substring(0, $width) }
|
|
1197
|
+
if (Test-GcrTuiActive) {
|
|
1198
|
+
Update-GcrTuiProgress -OkCount $OkCount -TotalCount $TotalCount -FailCount $FailCount -DoneBytes $DoneBytes -Rate $rate -Eta $eta -CurrentFile $CurrentFile
|
|
1199
|
+
Invoke-GcrTuiTick
|
|
1200
|
+
} else {
|
|
1201
|
+
Write-Host ("`r" + $out) -NoNewline
|
|
1202
|
+
Write-Progress -Activity "git-clone-resume" -Status $line -PercentComplete ([Math]::Min(100, [int]$pct))
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function Assert-GcrContinue {
|
|
1207
|
+
if (-not (Get-Command Test-GcrTuiActive -ErrorAction SilentlyContinue)) { return }
|
|
1208
|
+
if (-not (Test-GcrTuiActive)) { return }
|
|
1209
|
+
Invoke-GcrTuiTick
|
|
1210
|
+
Wait-GcrTuiPaused
|
|
1211
|
+
Invoke-GcrTuiTick
|
|
1212
|
+
if (Test-GcrTuiQuit) {
|
|
1213
|
+
$script:GcrUserStop = $true
|
|
1214
|
+
throw "已由用户停止。再次运行同一命令即可续传。"
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
$script:GcrInteractive = $false
|
|
1219
|
+
try { $script:GcrInteractive = [Environment]::UserInteractive } catch { }
|
|
1220
|
+
if ($NoTui) {
|
|
1221
|
+
$script:GcrTuiWanted = $false
|
|
1222
|
+
} elseif ($Tui) {
|
|
1223
|
+
$script:GcrTuiWanted = $true
|
|
1224
|
+
} elseif ($script:GcrInteractive -and (Get-Command Test-GcrTuiAvailable -ErrorAction SilentlyContinue) -and (Test-GcrTuiAvailable)) {
|
|
1225
|
+
$script:GcrTuiWanted = $true
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if ($ResumeLast) {
|
|
1229
|
+
if (-not (Get-Command Get-GcrHistoryLast -ErrorAction SilentlyContinue)) {
|
|
1230
|
+
Write-Host (Convert-GcrText "错误: 无法读取历史记录。") -ForegroundColor Red
|
|
1231
|
+
exit 2
|
|
1232
|
+
}
|
|
1233
|
+
$last = Get-GcrHistoryLast
|
|
1234
|
+
if ($null -eq $last) {
|
|
1235
|
+
Write-Host (Convert-GcrText "错误: 没有可恢复的历史记录。请先启动过一次克隆。") -ForegroundColor Red
|
|
1236
|
+
exit 2
|
|
1237
|
+
}
|
|
1238
|
+
if ([string]::IsNullOrWhiteSpace($RepoUrl) -and $last.url) { $RepoUrl = [string]$last.url }
|
|
1239
|
+
if ([string]::IsNullOrWhiteSpace($OutDir) -and $last.outDir) { $OutDir = [string]$last.outDir }
|
|
1240
|
+
if (($Ref -eq "HEAD" -or [string]::IsNullOrWhiteSpace($Ref)) -and $last.ref) { $Ref = [string]$last.ref }
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if ([string]::IsNullOrWhiteSpace($RepoUrl)) {
|
|
1244
|
+
if ($NoTui -or -not $script:GcrInteractive) {
|
|
1245
|
+
Show-Usage
|
|
1246
|
+
Write-Host (Convert-GcrText "错误: 必须提供仓库 URL。") -ForegroundColor Red
|
|
1247
|
+
exit 2
|
|
1248
|
+
}
|
|
1249
|
+
$defaults = @{
|
|
1250
|
+
RepoUrl = $RepoUrl
|
|
1251
|
+
OutDir = $OutDir
|
|
1252
|
+
Ref = $Ref
|
|
1253
|
+
BatchSize = $BatchSize
|
|
1254
|
+
MaxRetries = $MaxRetries
|
|
1255
|
+
Include = $Include
|
|
1256
|
+
Exclude = $Exclude
|
|
1257
|
+
Verify = [bool]$Verify
|
|
1258
|
+
ForceRefetch = [bool]$ForceRefetch
|
|
1259
|
+
DryRun = [bool]$DryRun
|
|
1260
|
+
}
|
|
1261
|
+
if ($PSBoundParameters.ContainsKey("Depth")) { $defaults["Depth"] = $Depth }
|
|
1262
|
+
if (-not (Get-Command Show-GcrInteractiveSetup -ErrorAction SilentlyContinue)) {
|
|
1263
|
+
Show-Usage
|
|
1264
|
+
Write-Host (Convert-GcrText "错误: 必须提供仓库 URL。") -ForegroundColor Red
|
|
1265
|
+
exit 2
|
|
1266
|
+
}
|
|
1267
|
+
$wiz = $null
|
|
1268
|
+
try {
|
|
1269
|
+
$wiz = Show-GcrInteractiveSetup -Defaults $defaults
|
|
1270
|
+
if (Get-Command ConvertFrom-GcrWizardOutput -ErrorAction SilentlyContinue) {
|
|
1271
|
+
$unwrapped = ConvertFrom-GcrWizardOutput $wiz
|
|
1272
|
+
if ($null -ne $unwrapped) { $wiz = $unwrapped }
|
|
1273
|
+
}
|
|
1274
|
+
} catch {
|
|
1275
|
+
if (Get-Command Close-GcrTui -ErrorAction SilentlyContinue) { Close-GcrTui }
|
|
1276
|
+
Write-Host ("向导失败: " + $_.Exception.Message) -ForegroundColor Red
|
|
1277
|
+
exit 1
|
|
1278
|
+
}
|
|
1279
|
+
if ($null -eq $wiz) {
|
|
1280
|
+
if (Get-Command Close-GcrTui -ErrorAction SilentlyContinue) { Close-GcrTui }
|
|
1281
|
+
exit 0
|
|
1282
|
+
}
|
|
1283
|
+
try {
|
|
1284
|
+
$RepoUrl = [string]$wiz.RepoUrl
|
|
1285
|
+
if ($wiz.Language -eq "en-US" -or $wiz.Language -eq "zh-CN") {
|
|
1286
|
+
Set-GcrLanguage -Language ([string]$wiz.Language)
|
|
1287
|
+
}
|
|
1288
|
+
if ($wiz.OutDir) { $OutDir = [string]$wiz.OutDir }
|
|
1289
|
+
if ($wiz.Ref) { $Ref = [string]$wiz.Ref }
|
|
1290
|
+
if ($wiz.BatchSize) { $BatchSize = [int]$wiz.BatchSize }
|
|
1291
|
+
if ($wiz.MaxRetries) { $MaxRetries = [int]$wiz.MaxRetries }
|
|
1292
|
+
if ($null -ne $wiz.Include) { $Include = @($wiz.Include | Where-Object { $_ }) }
|
|
1293
|
+
if ($null -ne $wiz.Exclude) { $Exclude = @($wiz.Exclude | Where-Object { $_ }) }
|
|
1294
|
+
$Verify = [bool]$wiz.Verify
|
|
1295
|
+
$ForceRefetch = [bool]$wiz.ForceRefetch
|
|
1296
|
+
$DryRun = [bool]$wiz.DryRun
|
|
1297
|
+
if ($null -ne $wiz.Depth -and [string]$wiz.Depth -ne "") {
|
|
1298
|
+
$Depth = [int]$wiz.Depth
|
|
1299
|
+
$PSBoundParameters["Depth"] = $Depth
|
|
1300
|
+
}
|
|
1301
|
+
} catch {
|
|
1302
|
+
if (Test-GcrTuiActive) {
|
|
1303
|
+
Show-GcrTuiResult -Title "无法开始克隆" -Body @($_.Exception.Message) -Kind error
|
|
1304
|
+
Close-GcrTui
|
|
1305
|
+
} else {
|
|
1306
|
+
Write-Host ("无法开始克隆: " + $_.Exception.Message) -ForegroundColor Red
|
|
1307
|
+
}
|
|
1308
|
+
exit 1
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if ([string]::IsNullOrWhiteSpace($RepoUrl)) {
|
|
1313
|
+
if (Get-Command Close-GcrTui -ErrorAction SilentlyContinue) { Close-GcrTui }
|
|
1314
|
+
Show-Usage
|
|
1315
|
+
Write-Host (Convert-GcrText "错误: 必须提供仓库 URL。") -ForegroundColor Red
|
|
1316
|
+
exit 2
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if ($script:GcrTuiWanted -and (Get-Command Initialize-GcrTui -ErrorAction SilentlyContinue)) {
|
|
1320
|
+
if (-not (Test-GcrTuiActive)) { [void](Initialize-GcrTui) }
|
|
1321
|
+
if ($Tui -and -not (Test-GcrTuiActive)) {
|
|
1322
|
+
Write-Host (Convert-GcrText "当前终端无法进入全屏 TUI,改用日志模式。Windows Terminal 下再试,或去掉 -Tui。") -ForegroundColor Yellow
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
try {
|
|
1327
|
+
if (-not $OutDir) { $OutDir = Get-RepoFolderName -Url $RepoUrl }
|
|
1328
|
+
$repoRoot = Convert-ToFullPath -Path $OutDir
|
|
1329
|
+
$script:RepoRoot = $repoRoot
|
|
1330
|
+
|
|
1331
|
+
if (Test-GcrTuiActive) {
|
|
1332
|
+
$resumeHint = Test-Path -LiteralPath (Join-Path $repoRoot ".git\$($script:StateDirName)\meta.txt")
|
|
1333
|
+
Set-GcrTuiRepo -Url $RepoUrl -OutDir $repoRoot -Ref $Ref -Resume:$resumeHint
|
|
1334
|
+
Set-GcrTuiPhase -Name "init" -Detail ""
|
|
1335
|
+
if (Get-Command Save-GcrHistory -ErrorAction SilentlyContinue) {
|
|
1336
|
+
Save-GcrHistory -Url $RepoUrl -OutDir $repoRoot -Ref $Ref -Status "running"
|
|
1337
|
+
}
|
|
1338
|
+
Invoke-GcrTuiTick -Force
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
Test-GitAvailable
|
|
1342
|
+
$script:GitExe = Get-GitExePath
|
|
1343
|
+
|
|
1344
|
+
Write-Log "仓库: $RepoUrl" "STEP"
|
|
1345
|
+
Write-Log "目录: $repoRoot" "INFO"
|
|
1346
|
+
Write-Log "引用: $Ref" "INFO"
|
|
1347
|
+
|
|
1348
|
+
if (-not (Test-Path -LiteralPath $repoRoot)) {
|
|
1349
|
+
New-Item -ItemType Directory -Path $repoRoot -Force | Out-Null
|
|
1350
|
+
}
|
|
1351
|
+
Initialize-PartialRepo -RepoRoot $repoRoot -Url $RepoUrl
|
|
1352
|
+
|
|
1353
|
+
$gitDir = Join-Path $repoRoot ".git"
|
|
1354
|
+
$stateDir = Join-Path $gitDir $script:StateDirName
|
|
1355
|
+
if (-not (Test-Path -LiteralPath $stateDir)) {
|
|
1356
|
+
New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
|
|
1357
|
+
}
|
|
1358
|
+
$metaPath = Join-Path $stateDir "meta.txt"
|
|
1359
|
+
$listPath = Join-Path $stateDir "files.tsv"
|
|
1360
|
+
$donePath = Join-Path $stateDir "done.txt"
|
|
1361
|
+
$failedPath = Join-Path $stateDir "failed.txt"
|
|
1362
|
+
$script:LogFile = Join-Path $stateDir "log.txt"
|
|
1363
|
+
|
|
1364
|
+
Clear-StaleIndexLock -RepoRoot $repoRoot
|
|
1365
|
+
|
|
1366
|
+
$fetchArgs = @(
|
|
1367
|
+
"-c", "http.version=HTTP/1.1",
|
|
1368
|
+
"fetch", "--filter=blob:none", "--progress", "--no-recurse-submodules"
|
|
1369
|
+
)
|
|
1370
|
+
if ($PSBoundParameters.ContainsKey("Depth")) {
|
|
1371
|
+
$fetchArgs += @("--depth", [string]$Depth)
|
|
1372
|
+
}
|
|
1373
|
+
$fetchArgs += @("origin", $Ref)
|
|
1374
|
+
|
|
1375
|
+
$needFetch = $true
|
|
1376
|
+
$pinnedSha = $null
|
|
1377
|
+
$meta = Read-Meta -MetaPath $metaPath
|
|
1378
|
+
if (-not $ForceRefetch -and $meta.ContainsKey("commit") -and $meta["ref"] -eq $Ref) {
|
|
1379
|
+
$trySha = $meta["commit"]
|
|
1380
|
+
$chk = Invoke-GitProcess -WorkDir $repoRoot -ExpectFail -GitArgs @("cat-file", "-t", $trySha)
|
|
1381
|
+
if ($chk.ExitCode -eq 0 -and $chk.StdOut.Trim() -eq "commit") {
|
|
1382
|
+
$needFetch = $false
|
|
1383
|
+
$pinnedSha = $trySha
|
|
1384
|
+
$short = $trySha.Substring(0, [Math]::Min(12, $trySha.Length))
|
|
1385
|
+
Write-Log "本地已有 commit $short ,跳过 fetch(需要更新请加 -ForceRefetch)" "OK"
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if ($needFetch) {
|
|
1390
|
+
Write-Log "fetch 元数据: git fetch --filter=blob:none origin $Ref" "STEP"
|
|
1391
|
+
Set-GcrTuiPhase -Name "fetch" -Detail ("origin " + $Ref)
|
|
1392
|
+
Assert-GcrContinue
|
|
1393
|
+
$inheritFetch = -not (Test-GcrTuiActive)
|
|
1394
|
+
Invoke-GitRetry -What "git fetch" -WorkDir $repoRoot -GitArgs $fetchArgs -InheritConsole:$inheritFetch | Out-Null
|
|
1395
|
+
$rev = Invoke-GitRetry -What "rev-parse FETCH_HEAD" -WorkDir $repoRoot -GitArgs @("rev-parse", "FETCH_HEAD")
|
|
1396
|
+
$pinnedSha = $rev.StdOut.Trim()
|
|
1397
|
+
if ([string]::IsNullOrWhiteSpace($pinnedSha)) {
|
|
1398
|
+
throw "无法解析 FETCH_HEAD,fetch 可能失败。"
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
$type = (Invoke-Git -WorkDir $repoRoot -GitArgs @("cat-file", "-t", $pinnedSha)).Trim()
|
|
1403
|
+
if ($type -ne "commit") {
|
|
1404
|
+
throw "目标 $Ref 解析为 $type ($pinnedSha),需要 commit。"
|
|
1405
|
+
}
|
|
1406
|
+
Set-DetachedHead -RepoRoot $repoRoot -Sha $pinnedSha
|
|
1407
|
+
Write-Log "目标 commit: $pinnedSha" "OK"
|
|
1408
|
+
Set-GcrTuiRepo -Commit $pinnedSha
|
|
1409
|
+
|
|
1410
|
+
Write-Meta -MetaPath $metaPath -Map @{
|
|
1411
|
+
url = $RepoUrl
|
|
1412
|
+
ref = $Ref
|
|
1413
|
+
commit = $pinnedSha
|
|
1414
|
+
batch = [string]$BatchSize
|
|
1415
|
+
updated = (Get-Date -Format o)
|
|
1416
|
+
hostname = [string]$env:COMPUTERNAME
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
Write-Log "枚举文件树 git ls-tree -r $pinnedSha" "STEP"
|
|
1420
|
+
Set-GcrTuiPhase -Name "list" -Detail $pinnedSha
|
|
1421
|
+
Assert-GcrContinue
|
|
1422
|
+
$all = Get-TreeEntries -RepoRoot $repoRoot -Sha $pinnedSha
|
|
1423
|
+
if ($null -eq $all) { $all = New-Object System.Collections.Generic.List[object] }
|
|
1424
|
+
Write-Log ("树中条目: " + $all.Count) "INFO"
|
|
1425
|
+
|
|
1426
|
+
$skippedSubmodule = 0
|
|
1427
|
+
$filtered = New-Object System.Collections.Generic.List[object]
|
|
1428
|
+
$includeArr = @($Include | Where-Object { $_ })
|
|
1429
|
+
$excludeArr = @($Exclude | Where-Object { $_ })
|
|
1430
|
+
foreach ($e in $all) {
|
|
1431
|
+
if ($e.Type -eq "commit" -or $e.Mode -eq "160000") {
|
|
1432
|
+
$skippedSubmodule++
|
|
1433
|
+
continue
|
|
1434
|
+
}
|
|
1435
|
+
if ($e.Type -ne "blob") { continue }
|
|
1436
|
+
if ($includeArr.Count -gt 0 -and -not (Test-WildcardMatch -Path $e.Path -Patterns $includeArr)) { continue }
|
|
1437
|
+
if ($excludeArr.Count -gt 0 -and (Test-WildcardMatch -Path $e.Path -Patterns $excludeArr)) { continue }
|
|
1438
|
+
[void]$filtered.Add($e)
|
|
1439
|
+
}
|
|
1440
|
+
if ($skippedSubmodule -gt 0) {
|
|
1441
|
+
Write-Log "跳过 $skippedSubmodule 个子模块(gitlink)。需要的话请在对应目录单独再跑本脚本。" "WARN"
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
Write-Log ("待处理文件: " + $filtered.Count + " (blob 体积在 checkout 后统计,避免 ls-tree -l 把全部 blob 拉下来)") "INFO"
|
|
1445
|
+
|
|
1446
|
+
$tsv = New-Object System.Text.StringBuilder
|
|
1447
|
+
[void]$tsv.AppendLine("mode" + [char]9 + "type" + [char]9 + "blob" + [char]9 + "size" + [char]9 + "path")
|
|
1448
|
+
foreach ($e in $filtered) {
|
|
1449
|
+
[void]$tsv.AppendLine($e.Mode + [char]9 + $e.Type + [char]9 + $e.Blob + [char]9 + $e.Size + [char]9 + $e.Path)
|
|
1450
|
+
}
|
|
1451
|
+
Save-TextFile -Path $listPath -Content $tsv.ToString()
|
|
1452
|
+
|
|
1453
|
+
$okCount = 0
|
|
1454
|
+
$failCount = 0
|
|
1455
|
+
$totalCount = $filtered.Count
|
|
1456
|
+
$doneBytes = 0L
|
|
1457
|
+
$skipDownload = $false
|
|
1458
|
+
$resultKind = "done"
|
|
1459
|
+
$resultTitle = ""
|
|
1460
|
+
$resultBody = New-Object System.Collections.Generic.List[string]
|
|
1461
|
+
|
|
1462
|
+
if ($DryRun) {
|
|
1463
|
+
Write-Log "DryRun 结束,清单: $listPath" "OK"
|
|
1464
|
+
$nshow = [Math]::Min(30, $filtered.Count)
|
|
1465
|
+
for ($i = 0; $i -lt $nshow; $i++) {
|
|
1466
|
+
$e = $filtered[$i]
|
|
1467
|
+
if (Test-GcrTuiActive) { Add-GcrTuiLog -Level "INFO" -Message $e.Path }
|
|
1468
|
+
else { Write-Host (" " + $e.Path) }
|
|
1469
|
+
}
|
|
1470
|
+
if ($filtered.Count -gt 30) {
|
|
1471
|
+
$more = (" ... 另有 {0} 个文件" -f ($filtered.Count - 30))
|
|
1472
|
+
if (Test-GcrTuiActive) { Add-GcrTuiLog -Level "INFO" -Message $more.Trim() }
|
|
1473
|
+
else { Write-Host $more }
|
|
1474
|
+
}
|
|
1475
|
+
$script:GcrExitCode = 0
|
|
1476
|
+
$skipDownload = $true
|
|
1477
|
+
$resultTitle = "DryRun 完成"
|
|
1478
|
+
[void]$resultBody.Add(("清单文件: " + $listPath))
|
|
1479
|
+
[void]$resultBody.Add(("文件数: " + $filtered.Count))
|
|
1480
|
+
[void]$resultBody.Add("未下载 blob。去掉 -DryRun 后开始/继续克隆。")
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
if (-not $skipDownload) {
|
|
1484
|
+
Set-GcrTuiPhase -Name "scan" -Detail "检查工作区已有文件"
|
|
1485
|
+
$doneSet = Load-DoneSet -DonePath $donePath
|
|
1486
|
+
$pending = New-Object System.Collections.Generic.List[object]
|
|
1487
|
+
$skippedDone = 0
|
|
1488
|
+
$skippedExist = 0
|
|
1489
|
+
$reverify = [bool]$Verify
|
|
1490
|
+
$scanTotal = $filtered.Count
|
|
1491
|
+
$scanIndex = 0
|
|
1492
|
+
$scanStarted = Get-Date
|
|
1493
|
+
|
|
1494
|
+
foreach ($e in $filtered) {
|
|
1495
|
+
$scanIndex++
|
|
1496
|
+
if (($scanIndex % 200) -eq 0 -or $scanIndex -eq $scanTotal) {
|
|
1497
|
+
Write-DownloadProgress -OkCount $scanIndex -TotalCount $scanTotal -FailCount 0 -DoneBytes 0L -Elapsed ((Get-Date) - $scanStarted) -DoneThisRun $scanIndex -CurrentFile ("scan " + $e.Path)
|
|
1498
|
+
Assert-GcrContinue
|
|
1499
|
+
}
|
|
1500
|
+
$complete = Test-FileComplete -RepoRoot $repoRoot -Entry $e -HashVerify:$reverify
|
|
1501
|
+
if ($complete) {
|
|
1502
|
+
if ($doneSet.Contains($e.Path)) {
|
|
1503
|
+
$skippedDone++
|
|
1504
|
+
} else {
|
|
1505
|
+
[void]$doneSet.Add($e.Path)
|
|
1506
|
+
Add-DonePaths -DonePath $donePath -Paths @($e.Path)
|
|
1507
|
+
$skippedExist++
|
|
1508
|
+
}
|
|
1509
|
+
continue
|
|
1510
|
+
}
|
|
1511
|
+
if ($doneSet.Contains($e.Path)) {
|
|
1512
|
+
[void]$doneSet.Remove($e.Path)
|
|
1513
|
+
}
|
|
1514
|
+
[void]$pending.Add($e)
|
|
1515
|
+
}
|
|
1516
|
+
Write-GcrNewline
|
|
1517
|
+
|
|
1518
|
+
Write-Log ("进度: 已记录 " + $skippedDone + " ,工作区已存在 " + $skippedExist + " ,剩余 " + $pending.Count) "INFO"
|
|
1519
|
+
|
|
1520
|
+
$okCount = $skippedDone + $skippedExist
|
|
1521
|
+
$totalCount = $filtered.Count
|
|
1522
|
+
foreach ($e in $filtered) {
|
|
1523
|
+
if ($doneSet.Contains($e.Path)) { $doneBytes += (Get-WorktreeBytes -RepoRoot $repoRoot -RelPath $e.Path) }
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
if ($pending.Count -eq 0) {
|
|
1527
|
+
Write-Log "全部文件已就绪。" "OK"
|
|
1528
|
+
Write-Log "工作区: $repoRoot" "OK"
|
|
1529
|
+
$script:GcrExitCode = 0
|
|
1530
|
+
$skipDownload = $true
|
|
1531
|
+
$resultTitle = "全部文件已就绪"
|
|
1532
|
+
[void]$resultBody.Add(("工作区: " + $repoRoot))
|
|
1533
|
+
[void]$resultBody.Add(("文件: {0}/{1}" -f $okCount, $totalCount))
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
if (-not $skipDownload) {
|
|
1538
|
+
$batches = Split-Batches -Items $pending.ToArray() -MaxCount $BatchSize -MaxChars $MaxArgChars
|
|
1539
|
+
Write-Log ("分 " + $batches.Count + " 批下载,每批最多 " + $BatchSize + " 个文件") "STEP"
|
|
1540
|
+
Set-GcrTuiPhase -Name "download" -Detail ("batch 1/" + $batches.Count)
|
|
1541
|
+
Assert-GcrContinue
|
|
1542
|
+
|
|
1543
|
+
$started = Get-Date
|
|
1544
|
+
$failCount = 0
|
|
1545
|
+
$processedThisRun = 0
|
|
1546
|
+
Write-DownloadProgress -OkCount $okCount -TotalCount $totalCount -FailCount 0 -DoneBytes $doneBytes -Elapsed ([TimeSpan]::Zero) -DoneThisRun 0 -CurrentFile "starting download"
|
|
1547
|
+
|
|
1548
|
+
$batchIndex = 0
|
|
1549
|
+
foreach ($batch in $batches) {
|
|
1550
|
+
Assert-GcrContinue
|
|
1551
|
+
$batchIndex++
|
|
1552
|
+
Set-GcrTuiPhase -Name "download" -Detail ("batch " + $batchIndex + "/" + $batches.Count)
|
|
1553
|
+
$okThis = New-Object System.Collections.Generic.List[object]
|
|
1554
|
+
$badThis = New-Object System.Collections.Generic.List[object]
|
|
1555
|
+
$preview = $batch[0].Path
|
|
1556
|
+
Write-DownloadProgress -OkCount $okCount -TotalCount $totalCount -FailCount $failCount -DoneBytes $doneBytes -Elapsed ((Get-Date) - $started) -DoneThisRun $processedThisRun -CurrentFile $preview
|
|
1557
|
+
|
|
1558
|
+
try {
|
|
1559
|
+
Invoke-CheckoutBatch -RepoRoot $repoRoot -Sha $pinnedSha -Batch $batch
|
|
1560
|
+
$result = Confirm-BatchFiles -RepoRoot $repoRoot -Batch $batch
|
|
1561
|
+
foreach ($x in $result.Ok) { [void]$okThis.Add($x) }
|
|
1562
|
+
foreach ($x in $result.Bad) { [void]$badThis.Add($x) }
|
|
1563
|
+
} catch {
|
|
1564
|
+
if ($script:GcrUserStop) { throw }
|
|
1565
|
+
$nBatch = @($batch).Count
|
|
1566
|
+
if ($nBatch -gt 1) {
|
|
1567
|
+
Write-Log ("批次 " + $batchIndex + "/" + $batches.Count + " 失败(" + $nBatch + " 个文件),立即拆成单文件,不再整批重试: " + $_.Exception.Message) "WARN"
|
|
1568
|
+
} else {
|
|
1569
|
+
Write-Log ("单文件失败: " + $_.Exception.Message) "WARN"
|
|
1570
|
+
}
|
|
1571
|
+
foreach ($x in $batch) { [void]$badThis.Add($x) }
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
$retry = New-Object System.Collections.Generic.List[object]
|
|
1575
|
+
foreach ($e in $badThis) { [void]$retry.Add($e) }
|
|
1576
|
+
foreach ($e in $retry) {
|
|
1577
|
+
Assert-GcrContinue
|
|
1578
|
+
$oneOk = $false
|
|
1579
|
+
$one = New-Object System.Collections.Generic.List[object]
|
|
1580
|
+
[void]$one.Add($e)
|
|
1581
|
+
try {
|
|
1582
|
+
Invoke-CheckoutBatch -RepoRoot $repoRoot -Sha $pinnedSha -Batch $one
|
|
1583
|
+
if (Test-FileComplete -RepoRoot $repoRoot -Entry $e) { $oneOk = $true }
|
|
1584
|
+
} catch {
|
|
1585
|
+
if ($script:GcrUserStop) { throw }
|
|
1586
|
+
Add-FailedPath -FailedPath $failedPath -RelPath $e.Path -Reason $_.Exception.Message
|
|
1587
|
+
}
|
|
1588
|
+
if ($oneOk) {
|
|
1589
|
+
[void]$okThis.Add($e)
|
|
1590
|
+
} else {
|
|
1591
|
+
$failCount++
|
|
1592
|
+
Add-GcrTuiFailure -Path $e.Path
|
|
1593
|
+
$fullFail = Get-WorktreePath -Root $repoRoot -Rel $e.Path
|
|
1594
|
+
$why = "no worktree file"
|
|
1595
|
+
if (Test-Path -LiteralPath $fullFail) { $why = "worktree file present but still incomplete" }
|
|
1596
|
+
Write-Log ("仍失败: " + $e.Path + " (" + $why + ")") "ERROR"
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
$okPaths = New-Object System.Collections.Generic.List[string]
|
|
1601
|
+
foreach ($x in $okThis) { [void]$okPaths.Add([string]$x.Path) }
|
|
1602
|
+
if ($okPaths.Count -gt 0) {
|
|
1603
|
+
Add-DonePaths -DonePath $donePath -Paths $okPaths.ToArray()
|
|
1604
|
+
foreach ($p in $okPaths) { [void]$doneSet.Add($p) }
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
$processedThisRun += $okThis.Count
|
|
1608
|
+
$okCount += $okThis.Count
|
|
1609
|
+
foreach ($e in $okThis) { $doneBytes += (Get-WorktreeBytes -RepoRoot $repoRoot -RelPath $e.Path) }
|
|
1610
|
+
|
|
1611
|
+
$lastName = $batch[$batch.Count - 1].Path
|
|
1612
|
+
Write-DownloadProgress -OkCount $okCount -TotalCount $totalCount -FailCount $failCount -DoneBytes $doneBytes -Elapsed ((Get-Date) - $started) -DoneThisRun $processedThisRun -CurrentFile $lastName
|
|
1613
|
+
if (($batchIndex % 20) -eq 0 -or $okCount -eq $totalCount) {
|
|
1614
|
+
$pctNow = 0.0
|
|
1615
|
+
if ($totalCount -gt 0) { $pctNow = 100.0 * $okCount / $totalCount }
|
|
1616
|
+
Write-GcrNewline
|
|
1617
|
+
Write-Log (("checkpoint {0}/{1} {2:N1}% fail {3} {4}" -f $okCount, $totalCount, $pctNow, $failCount, (Format-Bytes $doneBytes))) "INFO"
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
Write-GcrNewline
|
|
1622
|
+
if (-not (Test-GcrTuiActive)) {
|
|
1623
|
+
Write-Progress -Activity "git-clone-resume" -Completed
|
|
1624
|
+
}
|
|
1625
|
+
Set-GcrTuiPhase -Name "repair" -Detail "git index"
|
|
1626
|
+
Repair-GitIndex -RepoRoot $repoRoot -Sha $pinnedSha
|
|
1627
|
+
$elapsed = (Get-Date) - $started
|
|
1628
|
+
$elapsedText = "{0:00}:{1:00}:{2:00}" -f [int]$elapsed.TotalHours, $elapsed.Minutes, $elapsed.Seconds
|
|
1629
|
+
Write-Log ("完成: 成功 {0}/{1} ,失败 {2} ,耗时 {3}" -f $okCount, $totalCount, $failCount, $elapsedText) "OK"
|
|
1630
|
+
Write-Log "工作区: $repoRoot" "OK"
|
|
1631
|
+
[void]$resultBody.Add(("工作区: " + $repoRoot))
|
|
1632
|
+
[void]$resultBody.Add(("成功 {0}/{1} ,失败 {2} ,耗时 {3}" -f $okCount, $totalCount, $failCount, $elapsedText))
|
|
1633
|
+
if ($failCount -gt 0) {
|
|
1634
|
+
Write-Log "失败列表: $failedPath (再次运行本脚本会重试未完成文件)" "WARN"
|
|
1635
|
+
[void]$resultBody.Add(("失败列表: " + $failedPath))
|
|
1636
|
+
[void]$resultBody.Add("再次运行同一命令会重试未完成文件。")
|
|
1637
|
+
$script:GcrExitCode = 1
|
|
1638
|
+
$resultKind = "error"
|
|
1639
|
+
$resultTitle = "部分文件失败"
|
|
1640
|
+
} else {
|
|
1641
|
+
$script:GcrExitCode = 0
|
|
1642
|
+
$resultTitle = "克隆完成"
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (Get-Command Save-GcrHistory -ErrorAction SilentlyContinue) {
|
|
1647
|
+
$histStatus = "complete"
|
|
1648
|
+
if ($script:GcrExitCode -ne 0) { $histStatus = "failed" }
|
|
1649
|
+
if ($DryRun) { $histStatus = "dryrun" }
|
|
1650
|
+
Save-GcrHistory -Url $RepoUrl -OutDir $repoRoot -Ref $Ref -Commit $pinnedSha -Status $histStatus -Ok $okCount -Total $totalCount -Fail $failCount
|
|
1651
|
+
}
|
|
1652
|
+
if (Test-GcrTuiActive) {
|
|
1653
|
+
if (-not $resultTitle) { $resultTitle = "完成" }
|
|
1654
|
+
Show-GcrTuiResult -Title $resultTitle -Body $resultBody.ToArray() -Kind $resultKind
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
catch {
|
|
1658
|
+
$err = ""
|
|
1659
|
+
try { $err = [string]$_.Exception.Message } catch { }
|
|
1660
|
+
if (-not $err) { try { $err = [string]$_ } catch { $err = "unknown error" } }
|
|
1661
|
+
Write-Log $err "ERROR"
|
|
1662
|
+
if ($_.ScriptStackTrace -and -not $script:GcrUserStop) { Write-Log ([string]$_.ScriptStackTrace) "ERROR" }
|
|
1663
|
+
if ($script:RepoRoot) {
|
|
1664
|
+
Write-Log ("中断后续传: 重新执行同一命令即可。仓库目录: " + $script:RepoRoot) "WARN"
|
|
1665
|
+
}
|
|
1666
|
+
if (Get-Command Save-GcrHistory -ErrorAction SilentlyContinue -and $script:RepoRoot) {
|
|
1667
|
+
$st = "failed"
|
|
1668
|
+
if ($script:GcrUserStop) { $st = "partial" }
|
|
1669
|
+
Save-GcrHistory -Url $RepoUrl -OutDir $script:RepoRoot -Ref $Ref -Status $st
|
|
1670
|
+
}
|
|
1671
|
+
if (Test-GcrTuiActive) {
|
|
1672
|
+
$body = @($err)
|
|
1673
|
+
if ($script:RepoRoot) { $body += ("仓库目录: " + $script:RepoRoot) }
|
|
1674
|
+
$body += "再次运行同一命令即可续传。"
|
|
1675
|
+
$title = $(if ($script:GcrUserStop) { "已停止" } else { "出错" })
|
|
1676
|
+
Show-GcrTuiResult -Title $title -Body $body -Kind "error"
|
|
1677
|
+
}
|
|
1678
|
+
$script:GcrExitCode = 1
|
|
1679
|
+
}
|
|
1680
|
+
finally {
|
|
1681
|
+
if (Get-Command Close-GcrTui -ErrorAction SilentlyContinue) { Close-GcrTui }
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
exit $script:GcrExitCode
|
|
1685
|
+
|