@peiyanlu/git-utils 0.0.0-stage → 0.1.0

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/dist/index.mjs ADDED
@@ -0,0 +1,1096 @@
1
+ import { runGit, runGitSync } from "@peiyanlu/command-utils";
2
+ import { splitNonEmptyLines } from "@peiyanlu/ts-utils";
3
+ // src/raw.ts
4
+ /**
5
+ * 引用解析操作
6
+ * @defaults `git rev-parse <args>`
7
+ */
8
+ const gitRevParse = async (args, options) => {
9
+ return runGit(["rev-parse", ...args], options);
10
+ };
11
+ /** {@link gitRevParse} 的同步版本 */
12
+ const gitRevParseSync = (args, options) => {
13
+ return runGitSync(["rev-parse", ...args], options);
14
+ };
15
+ /**
16
+ * 提交遍历操作
17
+ * @defaults `git rev-list <args>`
18
+ */
19
+ const gitRevList = async (args, options) => {
20
+ return runGit(["rev-list", ...args], options);
21
+ };
22
+ /** {@link gitRevList} 的同步版本 */
23
+ const gitRevListSync = (args, options) => {
24
+ return runGitSync(["rev-list", ...args], options);
25
+ };
26
+ /**
27
+ * 引用遍历操作
28
+ * @defaults `git for-each-ref <args>`
29
+ */
30
+ const gitForEachRef = async (args, options) => {
31
+ return runGit(["for-each-ref", ...args], options);
32
+ };
33
+ /** {@link gitForEachRef} 的同步版本 */
34
+ const gitForEachRefSync = (args, options) => {
35
+ return runGitSync(["for-each-ref", ...args], options);
36
+ };
37
+ /**
38
+ * 暂存操作
39
+ * @defaults `git add <args>`
40
+ */
41
+ const gitAdd = async (args, options) => {
42
+ return runGit(["add", ...args], options);
43
+ };
44
+ /** {@link gitAdd} 的同步版本 */
45
+ const gitAddSync = (args, options) => {
46
+ return runGitSync(["add", ...args], options);
47
+ };
48
+ /**
49
+ * 分支操作
50
+ * @defaults `git branch <args>`
51
+ */
52
+ const gitBranch = async (args, options) => {
53
+ return runGit(["branch", ...args], options);
54
+ };
55
+ /** {@link gitBranch} 的同步版本 */
56
+ const gitBranchSync = (args, options) => {
57
+ return runGitSync(["branch", ...args], options);
58
+ };
59
+ /**
60
+ * 迁出操作
61
+ * @defaults `git checkout <args>`
62
+ */
63
+ const gitCheckout = async (args, options) => {
64
+ return runGit(["checkout", ...args], options);
65
+ };
66
+ /** {@link gitCheckout} 的同步版本 */
67
+ const gitCheckoutSync = (args, options) => {
68
+ return runGitSync(["checkout", ...args], options);
69
+ };
70
+ /**
71
+ * 提交操作
72
+ * @defaults `git commit <args>`
73
+ */
74
+ const gitCommit = async (args = [], options) => {
75
+ return runGit(["commit", ...args], options);
76
+ };
77
+ /** {@link gitCommit} 的同步版本 */
78
+ const gitCommitSync = (args = [], options) => {
79
+ return runGitSync(["commit", ...args], options);
80
+ };
81
+ /**
82
+ * 配置操作
83
+ * @defaults `git config <args>`
84
+ */
85
+ const gitConfig = async (args, options) => {
86
+ return runGit(["config", ...args], options);
87
+ };
88
+ /** {@link gitConfig} 的同步版本 */
89
+ const gitConfigSync = (args, options) => {
90
+ return runGitSync(["config", ...args], options);
91
+ };
92
+ /**
93
+ * 符号引用操作
94
+ * @defaults `git symbolic-ref <args>`
95
+ */
96
+ const gitSymbolicRef = async (args, options) => {
97
+ return runGit(["symbolic-ref", ...args], options);
98
+ };
99
+ /** {@link gitSymbolicRef} 的同步版本 */
100
+ const gitSymbolicRefSync = (args, options) => {
101
+ return runGitSync(["symbolic-ref", ...args], options);
102
+ };
103
+ /**
104
+ * 引用更新操作
105
+ * @defaults `git update-ref <args>`
106
+ */
107
+ const gitUpdateRef = async (args, options) => {
108
+ return runGit(["update-ref", ...args], options);
109
+ };
110
+ /** {@link gitUpdateRef} 的同步版本 */
111
+ const gitUpdateRefSync = (args, options) => {
112
+ return runGitSync(["update-ref", ...args], options);
113
+ };
114
+ /**
115
+ * 推送操作
116
+ * @defaults `git push <args>`
117
+ */
118
+ const gitPush = async (args, options) => {
119
+ return runGit(["push", ...args], options);
120
+ };
121
+ /** {@link gitPush} 的同步版本 */
122
+ const gitPushSync = (args, options) => {
123
+ return runGitSync(["push", ...args], options);
124
+ };
125
+ /**
126
+ * 远程操作
127
+ * @defaults `git remote <args>`
128
+ */
129
+ const gitRemote = async (args, options) => {
130
+ return runGit(["remote", ...args], options);
131
+ };
132
+ /** {@link gitRemote} 的同步版本 */
133
+ const gitRemoteSync = (args, options) => {
134
+ return runGitSync(["remote", ...args], options);
135
+ };
136
+ /**
137
+ * tag 操作
138
+ * @defaults `git tag <args>`
139
+ */
140
+ const gitTag = async (args, options) => {
141
+ return runGit(["tag", ...args], options);
142
+ };
143
+ /** {@link gitTag} 的同步版本 */
144
+ const gitTagSync = (args, options) => {
145
+ return runGitSync(["tag", ...args], options);
146
+ };
147
+ /**
148
+ * 重置操作
149
+ * @defaults `git reset <args>`
150
+ */
151
+ const gitReset = async (args, options) => {
152
+ return runGit(["reset", ...args], options);
153
+ };
154
+ /** {@link gitReset} 的同步版本 */
155
+ const gitResetSync = (args, options) => {
156
+ return runGitSync(["reset", ...args], options);
157
+ };
158
+ /**
159
+ * 恢复操作
160
+ * @defaults `git restore <args>`
161
+ */
162
+ const gitRestore = async (args, options) => {
163
+ return runGit(["restore", ...args], options);
164
+ };
165
+ /** {@link gitRestore} 的同步版本 */
166
+ const gitRestoreSync = (args, options) => {
167
+ return runGitSync(["restore", ...args], options);
168
+ };
169
+ /**
170
+ * 回退操作
171
+ * @defaults `git revert <args>`
172
+ */
173
+ const gitRevert = async (args, options) => {
174
+ return runGit(["revert", ...args], options);
175
+ };
176
+ /** {@link gitRevert} 的同步版本 */
177
+ const gitRevertSync = (args, options) => {
178
+ return runGitSync(["revert", ...args], options);
179
+ };
180
+ /**
181
+ * 远程引用查询操作
182
+ * @defaults `git ls-remote <args>`
183
+ */
184
+ const gitLsRemote = async (args, options) => {
185
+ return runGit(["ls-remote", ...args], options);
186
+ };
187
+ /** {@link gitLsRemote} 的同步版本 */
188
+ const gitLsRemoteSync = (args, options) => {
189
+ return runGitSync(["ls-remote", ...args], options);
190
+ };
191
+ /**
192
+ * 状态查询操作
193
+ * @defaults `git status <args>`
194
+ */
195
+ const gitStatus = async (args = [], options) => {
196
+ return runGit(["status", ...args], options);
197
+ };
198
+ /** {@link gitStatus} 的同步版本 */
199
+ const gitStatusSync = (args = [], options) => {
200
+ return runGitSync(["status", ...args], options);
201
+ };
202
+ /**
203
+ * 对象查看操作
204
+ * @defaults `git show <args>`
205
+ */
206
+ const gitShow = async (args, options) => {
207
+ return runGit(["show", ...args], options);
208
+ };
209
+ /** {@link gitShow} 的同步版本 */
210
+ const gitShowSync = (args, options) => {
211
+ return runGitSync(["show", ...args], options);
212
+ };
213
+ /**
214
+ * 差异比较操作
215
+ * @defaults `git diff <args>`
216
+ */
217
+ const gitDiff = async (args = [], options) => {
218
+ return runGit(["diff", ...args], options);
219
+ };
220
+ /** {@link gitDiff} 的同步版本 */
221
+ const gitDiffSync = (args = [], options) => {
222
+ return runGitSync(["diff", ...args], options);
223
+ };
224
+ /**
225
+ * 暂存区保存操作
226
+ * @defaults `git stash <args>`
227
+ */
228
+ const gitStash = async (args = [], options) => {
229
+ return runGit(["stash", ...args], options);
230
+ };
231
+ /** {@link gitStash} 的同步版本 */
232
+ const gitStashSync = (args = [], options) => {
233
+ return runGitSync(["stash", ...args], options);
234
+ };
235
+ /**
236
+ * 删除文件操作
237
+ * @defaults `git rm <args>`
238
+ */
239
+ const gitRm = async (args, options) => {
240
+ return runGit(["rm", ...args], options);
241
+ };
242
+ /** {@link gitRm} 的同步版本 */
243
+ const gitRmSync = (args, options) => {
244
+ return runGitSync(["rm", ...args], options);
245
+ };
246
+ /**
247
+ * 移动文件操作
248
+ * @defaults `git mv <args>`
249
+ */
250
+ const gitMv = async (args, options) => {
251
+ return runGit(["mv", ...args], options);
252
+ };
253
+ /** {@link gitMv} 的同步版本 */
254
+ const gitMvSync = (args, options) => {
255
+ return runGitSync(["mv", ...args], options);
256
+ };
257
+ /**
258
+ * 对象内容操作
259
+ * @defaults `git cat-file <args>`
260
+ */
261
+ const gitCatFile = async (args, options) => {
262
+ return runGit(["cat-file", ...args], options);
263
+ };
264
+ /** {@link gitCatFile} 的同步版本 */
265
+ const gitCatFileSync = (args, options) => {
266
+ return runGitSync(["cat-file", ...args], options);
267
+ };
268
+ /**
269
+ * 日志查询操作
270
+ * @defaults `git log <args>`
271
+ */
272
+ const gitLog = async (args, options) => {
273
+ return runGit(["log", ...args], options);
274
+ };
275
+ /** {@link gitLog} 的同步版本 */
276
+ const gitLogSync = (args, options) => {
277
+ return runGitSync(["log", ...args], options);
278
+ };
279
+ /**
280
+ * 仓库初始化操作
281
+ * @defaults `git init <args>`
282
+ */
283
+ const gitInit = async (args, options) => {
284
+ return runGit(["init", ...args], options);
285
+ };
286
+ /** {@link gitInit} 的同步版本 */
287
+ const gitInitSync = (args, options) => {
288
+ return runGitSync(["init", ...args], options);
289
+ };
290
+ /**
291
+ * 获取远程更新操作
292
+ * @defaults `git fetch <args>`
293
+ */
294
+ const gitFetch = async (args, options) => {
295
+ return runGit(["fetch", ...args], options);
296
+ };
297
+ /** {@link gitFetch} 的同步版本 */
298
+ const gitFetchSync = (args, options) => {
299
+ return runGitSync(["fetch", ...args], options);
300
+ };
301
+ // src/add.ts
302
+ /**
303
+ * 暂存所有文件
304
+ * @defaults `git add --all`
305
+ */
306
+ const gitAddAll = async (args = []) => {
307
+ await gitAdd(["--all", ...args]);
308
+ };
309
+ /**
310
+ * 暂存已追踪文件
311
+ * @defaults `git add --update`
312
+ */
313
+ const gitAddTracked = async (args = []) => {
314
+ await gitAdd(["--update", ...args]);
315
+ };
316
+ // src/branch.ts
317
+ /**
318
+ * 获取当前分支
319
+ * @defaults `git branch --show-current`
320
+ * */
321
+ const gitBranchCurrent = async () => {
322
+ const branch = await gitBranch(["--show-current"]);
323
+ if (!branch) throw new Error("Detached HEAD");
324
+ return branch;
325
+ };
326
+ /**
327
+ * 重命名本地分支
328
+ * @defaults `git branch -m <old-name> <new-name>`
329
+ */
330
+ const gitBranchMove = async (oldName, newName) => {
331
+ await gitBranch([
332
+ "-m",
333
+ oldName,
334
+ newName
335
+ ]);
336
+ };
337
+ /**
338
+ * 重命名本地分支(强制)
339
+ * @defaults `git branch -M <old-name> <new-name>`
340
+ */
341
+ const gitBranchMoveForce = async (oldName, newName) => {
342
+ await gitBranch([
343
+ "-M",
344
+ oldName,
345
+ newName
346
+ ]);
347
+ };
348
+ /**
349
+ * 删除本地分支
350
+ * @defaults `git branch -d <branch>`
351
+ */
352
+ const gitBranchDelete = async (branch) => {
353
+ await gitBranch(["-d", branch]);
354
+ };
355
+ /**
356
+ * 删除本地分支(强制)
357
+ * @defaults `git branch -D <branch>`
358
+ */
359
+ const gitBranchDeleteForce = async (branch) => {
360
+ await gitBranch(["-D", branch]);
361
+ };
362
+ /** 获取所有分支 */
363
+ const getLocalBranches = async () => {
364
+ const res = await gitBranch(["--format", "%(refname:short)"]);
365
+ return splitNonEmptyLines(res);
366
+ };
367
+ const getRemoteBranches = async () => {
368
+ const res = await gitBranch([
369
+ "--remotes",
370
+ "--format",
371
+ "%(refname:short)"
372
+ ]);
373
+ return splitNonEmptyLines(res);
374
+ };
375
+ // src/checkout.ts
376
+ /**
377
+ * 创建并签出
378
+ * @defaults `git checkout -b <branch> <start-point>`
379
+ * @fallback `git checkout <branch>`
380
+ * @see
381
+ * ```
382
+ * git branch <branch> <start-point>
383
+ * git checkout <branch>
384
+ * ```
385
+ */
386
+ const gitCheckoutBranch = async (branch, startpoint) => {
387
+ await gitCheckout([
388
+ "-b",
389
+ branch,
390
+ ...startpoint ? [startpoint] : []
391
+ ], { error: "throw" }).catch((_) => gitCheckout([branch], { error: "throw" }));
392
+ };
393
+ /**
394
+ * 强制创建/重置并签出
395
+ * @defaults `git checkout -B <branch> <start-point>`
396
+ * @see
397
+ * ```
398
+ * git checkout <start-point>
399
+ * git branch -f <branch> HEAD
400
+ * git checkout <branch>
401
+ * ```
402
+ */
403
+ const gitCheckoutBranchForce = async (branch, startpoint) => {
404
+ await gitCheckout([
405
+ "-B",
406
+ branch,
407
+ ...startpoint ? [startpoint] : []
408
+ ]);
409
+ };
410
+ /**
411
+ * 创建孤儿分支
412
+ * @defaults `git checkout --orphan <branch>`
413
+ */
414
+ const gitCheckoutBranchOrphan = async (branch, startpoint) => {
415
+ await gitCheckout([
416
+ "--orphan",
417
+ branch,
418
+ ...startpoint ? [startpoint] : []
419
+ ]);
420
+ };
421
+ // src/commit.ts
422
+ /**
423
+ * 提交
424
+ * @defaults `git commit --message message <args>`
425
+ */
426
+ const gitCommitMessage = async (message, args = []) => {
427
+ await gitCommit([
428
+ "--message",
429
+ message,
430
+ ...args
431
+ ]);
432
+ };
433
+ /**
434
+ * 空内容提交
435
+ * @defaults `git commit --message message --allow-empty <args>`
436
+ */
437
+ const gitCommitAllowEmpty = async (message, args = []) => {
438
+ await gitCommitMessage(message, ["--allow-empty", ...args]);
439
+ };
440
+ /**
441
+ * 修正提交
442
+ * @defaults `git commit --message message --amend <args>`
443
+ */
444
+ const gitCommitAmend = async (message, args = []) => {
445
+ await gitCommitMessage(message, ["--amend", ...args]);
446
+ };
447
+ /**
448
+ * 修正提交 保留信息
449
+ * @defaults `git commit --amend --no-edit <args>`
450
+ */
451
+ const gitCommitAmendNoEdit = async (args = []) => {
452
+ await gitCommit([
453
+ "--amend",
454
+ "--no-edit",
455
+ ...args
456
+ ]);
457
+ };
458
+ // src/config.ts
459
+ /**
460
+ * 获取指定的 git 配置
461
+ * @defaults `git config [--global] --get <key>`
462
+ */
463
+ const gitConfigGet = async (key, global = false) => {
464
+ return gitConfig([
465
+ ...global ? ["--global"] : [],
466
+ "--get",
467
+ key
468
+ ]);
469
+ };
470
+ /**
471
+ * 指定的 git 配置
472
+ * @defaults `git config [--global] <key> <value>`
473
+ */
474
+ const gitConfigSet = async (key, value, global = false) => {
475
+ await gitConfig([
476
+ ...global ? ["--global"] : [],
477
+ key,
478
+ value
479
+ ]);
480
+ };
481
+ /**
482
+ * 移除指定配置
483
+ * @defaults `git config [--global] --unset <key>`
484
+ */
485
+ const gitConfigUnset = async (key, global = false) => {
486
+ await gitConfig([
487
+ ...global ? ["--global"] : [],
488
+ "--unset",
489
+ key
490
+ ]);
491
+ };
492
+ /**
493
+ * 配置列表
494
+ * @defaults `git config [--global] --list`
495
+ */
496
+ const gitConfigList = async (global = false) => {
497
+ return gitConfig([...global ? ["--global"] : [], "--list"]);
498
+ };
499
+ // src/tag.ts
500
+ /**
501
+ * 创建 lightweight tag
502
+ * @defaults `git tag <tag> <args>`
503
+ */
504
+ const gitTagLightweight = async (tag, args = []) => {
505
+ await gitTag([tag, ...args]);
506
+ };
507
+ /**
508
+ * 创建 annotated tag
509
+ * @defaults `git tag --annotate --message <msg> <tag> <args>`
510
+ */
511
+ const gitTagAnnotated = async (tag, message = tag, args = []) => {
512
+ await gitTag([
513
+ "--annotate",
514
+ "--message",
515
+ message,
516
+ tag,
517
+ ...args
518
+ ]);
519
+ };
520
+ /**
521
+ * 创建 signed tag
522
+ * @defaults `git tag --sign --message <msg> <tag> <args>`
523
+ */
524
+ const gitTagSigned = async (tag, message = tag, args = []) => {
525
+ await gitTag([
526
+ "--sign",
527
+ "--message",
528
+ message,
529
+ tag,
530
+ ...args
531
+ ]);
532
+ };
533
+ /**
534
+ * 删除本地 tag
535
+ * @defaults `git tag --delete <tag> <args>`
536
+ */
537
+ const gitTagDelete = async (tag, args = []) => {
538
+ await gitTag([
539
+ "--delete",
540
+ tag,
541
+ ...args
542
+ ]);
543
+ };
544
+ /** {@link gitTagDelete} 的同步版本 */
545
+ const gitTagDeleteSync = (tag, args = []) => {
546
+ gitTagSync([
547
+ "--delete",
548
+ tag,
549
+ ...args
550
+ ]);
551
+ };
552
+ let GitSorter = /* @__PURE__ */ function(GitSorter) {
553
+ GitSorter["NAME_ASC"] = "v:refname";
554
+ GitSorter["NAME_DESC"] = "-v:refname";
555
+ GitSorter["DATE_ASC"] = "creatordate";
556
+ GitSorter["DATE_DESC"] = "-creatordate";
557
+ return GitSorter;
558
+ }({});
559
+ /**
560
+ * 获取本地所有 tag
561
+ * @defaults `git for-each-ref refs/tags/<match> --format "%(refname:short)" --sort <sort>
562
+ * --exclude refs/tags/<match> --count <count>`
563
+ */
564
+ const getLocalTags = async (match = "*", exclude = "*-beta.*", sort = "-v:refname", count = 0) => {
565
+ const excludes = [exclude].flat().flatMap((k) => [`--exclude`, `refs/tags/${k}`]);
566
+ const res = await gitForEachRef([
567
+ `refs/tags/${match}`,
568
+ ...excludes,
569
+ "--format",
570
+ "%(refname:short)",
571
+ `--sort`,
572
+ `${sort}`,
573
+ `--count`,
574
+ `${count}`
575
+ ]);
576
+ return res ? splitNonEmptyLines(res) : [];
577
+ };
578
+ /**
579
+ * 获取远程所有 tag
580
+ * @defaults `git ls-remote --tags --refs --sort <sort> <remote>`
581
+ */
582
+ const getRemoteTags = async (remote = "origin", sort = "-v:refname") => {
583
+ const res = await gitLsRemote([
584
+ "--tags",
585
+ "--refs",
586
+ "--sort",
587
+ sort,
588
+ remote
589
+ ]);
590
+ return res ? splitNonEmptyLines(res).map((t) => t.split("refs/tags/")[1]) : [];
591
+ };
592
+ /** 获取最新 tag */
593
+ const getLatestTag = async (match = "*", exclude = "*-beta.*") => {
594
+ const [latestTag] = await getLocalTags(match, exclude, "-v:refname", 1);
595
+ return latestTag;
596
+ };
597
+ /** 获取上一个 tag */
598
+ const getPreviousTag = async (tag, match = "*", exclude = "*-beta.*") => {
599
+ const all = await getLocalTags(match, exclude, "-v:refname", 0);
600
+ return all[all.findIndex((k) => tag === k) + 1];
601
+ };
602
+ // src/misc.ts
603
+ /** 初始化裸仓库,模拟远程仓库 */
604
+ const initBareRepo = async (dir) => {
605
+ await gitInit(["--bare", dir]);
606
+ };
607
+ /** 初始化仓库 */
608
+ const initRepo = async (branch = "master") => {
609
+ await gitInit(["--initial-branch", branch]);
610
+ };
611
+ /** 判断指定目录是否是 git 仓库 */
612
+ const isGitRepo = async (dir = ".") => {
613
+ return runGit([
614
+ "-C",
615
+ dir,
616
+ "status"
617
+ ], {
618
+ error: "throw",
619
+ trimEnd: true
620
+ }).then((_) => true).catch((_) => false);
621
+ };
622
+ /** 判断指定目录是否是 git 裸仓库 */
623
+ const isGitBareRepo = async (dir = ".") => {
624
+ return gitRevParse(["--is-bare-repository"], {
625
+ trimEnd: true,
626
+ cwd: dir
627
+ }).then((res) => "true" === res);
628
+ };
629
+ /** 判断工作区是否干净 */
630
+ const isWorkingTreeClean = async () => {
631
+ const status = await gitStatus(["--porcelain"]);
632
+ return !Boolean(status);
633
+ };
634
+ /**
635
+ * 判断当前分支是否已设置 upstream
636
+ * @defaults `git rev-parse --abbrev-ref --symbolic-full-name "<branch>@{u}"`
637
+ */
638
+ const hasUpstream = async (branch) => {
639
+ const ref = branch ? `${branch}@{u}` : "@{u}";
640
+ const upstream = await gitRevParse([
641
+ "--abbrev-ref",
642
+ "--symbolic-full-name",
643
+ ref
644
+ ]);
645
+ return Boolean(upstream);
646
+ };
647
+ /**
648
+ * 获取完整 hash
649
+ * @defaults `git rev-parse <rev>`
650
+ */
651
+ const getFullHash = async (rev) => {
652
+ return gitRevParse([rev]);
653
+ };
654
+ /**
655
+ * 获取短 hash
656
+ * @defaults `git rev-parse --short <rev>`
657
+ */
658
+ const getShortHash = async (rev) => {
659
+ return gitRevParse(["--short", rev]);
660
+ };
661
+ /**
662
+ * 提取所有分支
663
+ * @defaults `git fetch --all --prune`
664
+ */
665
+ const fetchAllPrune = async () => {
666
+ await gitFetch(["--all", "--prune"]);
667
+ };
668
+ /**
669
+ * 获取当前工作区状态
670
+ * @defaults `git status --short --untracked-files=no`
671
+ */
672
+ const getShortStatus = async () => {
673
+ return gitStatus(["--short"]);
674
+ };
675
+ /**
676
+ * 为 git status / changeset 输出添加颜色
677
+ *
678
+ * M -> 黄色(修改)
679
+ * A -> 绿色(新增)
680
+ * D -> 红色(删除)
681
+ * @param {string} log `git status --short` 输出
682
+ * @returns {string}
683
+ */
684
+ const coloredStatus = (log) => {
685
+ const colorStatusChar = (ch) => {
686
+ switch (ch) {
687
+ case "M": return `\x1b[33m${ch}\x1b[39m`;
688
+ case "A": return `\x1b[32m${ch}\x1b[39m`;
689
+ case "D": return `\x1b[31m${ch}\x1b[39m`;
690
+ case "?": return `\x1b[34m${ch}\x1b[39m`;
691
+ default: return ch;
692
+ }
693
+ };
694
+ const colorStatus = (status) => status.split("").map(colorStatusChar).join("");
695
+ return splitNonEmptyLines(log).map((line) => {
696
+ const status = line.slice(0, 2);
697
+ const file = line.slice(3);
698
+ return `${colorStatus(status)} \x1b[2m${file}\x1b[22m`;
699
+ }).join("\n");
700
+ };
701
+ /**
702
+ * 计算 changelog 的 commit 范围
703
+ * @param {boolean} isIncrement 是否为版本递增发布
704
+ * @param {string} match tag match
705
+ * @param {string} exclude tag exclude
706
+ * @returns {Promise<{from: string, to: string}>}
707
+ */
708
+ const resolveChangelogRange = async (isIncrement = true, match = "*", exclude = "*-beta.*") => {
709
+ const latestTag = await getLatestTag(match, exclude);
710
+ if (!latestTag) return {
711
+ from: "",
712
+ to: "HEAD"
713
+ };
714
+ const previousTag = await getPreviousTag(latestTag);
715
+ if (!isIncrement && previousTag) return {
716
+ from: previousTag,
717
+ to: `${latestTag}^1`
718
+ };
719
+ return {
720
+ from: latestTag,
721
+ to: "HEAD"
722
+ };
723
+ };
724
+ /**
725
+ * 获取指定范围内的 commit 日志
726
+ * @defaults `git log --pretty=format:"* %s (%h)" <from>...<to> -- <scope>`
727
+ */
728
+ const getLogSince = async (from = "", to = "HEAD", scope) => {
729
+ const cmd = ["--pretty=format:* %s (%h)"];
730
+ if (from) cmd.push(`${from}...${to}`);
731
+ if (scope) cmd.push(...["--", scope]);
732
+ return gitLog(cmd);
733
+ };
734
+ /** 判断字符串是否为合法 remote 名称 */
735
+ const isRemoteName = async (remote) => {
736
+ const res = await gitRevParse(["--verify", `refs/remotes/${remote}`]);
737
+ return Boolean(res);
738
+ };
739
+ /** 统计自 tag 以来的提交数量 */
740
+ const countCommitsSince = async (tag) => {
741
+ const ref = tag ? `${tag}...HEAD` : "HEAD";
742
+ return gitRevList([ref, "--count"], { error: "throw" }).then(Number).catch((_) => 0);
743
+ };
744
+ /**
745
+ * 将 HEAD 指向指定分支(不会切换工作区)
746
+ */
747
+ const gitSetHeadBranch = async (branch) => {
748
+ await gitSymbolicRef(["HEAD", `refs/heads/${branch}`]);
749
+ };
750
+ /** 删除当前分支引用 */
751
+ const gitDeleteHeadRef = async () => {
752
+ await gitUpdateRef(["-d", "HEAD"]);
753
+ };
754
+ /**
755
+ * 创建一个没有任何提交的分支状态。
756
+ */
757
+ const createUnbornBranch = async (branch) => {
758
+ await gitSymbolicRef(["HEAD", `refs/heads/${branch}`]);
759
+ await gitUpdateRef(["-d", "HEAD"]);
760
+ };
761
+ // src/push.ts
762
+ /**
763
+ * 推送 tag 到远程
764
+ * @defaults `git push <remote> refs/tags/<tag> <args>`
765
+ */
766
+ const gitPushTag = async (remote, tag, args = []) => {
767
+ await gitPush([
768
+ remote,
769
+ `refs/tags/${tag}`,
770
+ ...args
771
+ ]);
772
+ };
773
+ /**
774
+ * 推送分支到远程
775
+ * 自动处理 upstream
776
+ * @defaults `git push <remote> <branch> [--set-upstream] <args>`
777
+ */
778
+ const gitPushBranch = async (remote, branch, args = []) => {
779
+ const u = !await hasUpstream() ? ["--set-upstream"] : [];
780
+ await gitPush([
781
+ remote,
782
+ branch,
783
+ ...u,
784
+ ...args
785
+ ]);
786
+ };
787
+ /**
788
+ * 删除远程引用(分支、tag)
789
+ * @defaults `git push <remote> <ref> --delete <args>`
790
+ */
791
+ const gitPushDeleteRef = async (remote, ref, args = []) => {
792
+ await gitPush([
793
+ remote,
794
+ ref,
795
+ "--delete",
796
+ ...args
797
+ ]);
798
+ };
799
+ // src/remote.ts
800
+ /**
801
+ * 获取 url
802
+ * @defaults `git remote get-url <args>`
803
+ */
804
+ const gitRemoteGetUrl = async (args) => {
805
+ return gitRemote(["get-url", ...args]);
806
+ };
807
+ /**
808
+ * 设置 url
809
+ * @defaults `git remote set-url <args>`
810
+ */
811
+ const gitRemoteSetUrl = async (args) => {
812
+ await gitRemote(["set-url", ...args]);
813
+ };
814
+ /**
815
+ * 添加远程
816
+ * @defaults `git remote add <name> <url>`
817
+ */
818
+ const gitRemoteAdd = async (name, url) => {
819
+ await gitRemote([
820
+ "add",
821
+ name,
822
+ url
823
+ ]);
824
+ };
825
+ /**
826
+ * 重命名远程
827
+ * @defaults `git remote rename <old-name> <new-name>`
828
+ */
829
+ const gitRemoteRename = async (oldName, newName) => {
830
+ await gitRemote([
831
+ "rename",
832
+ oldName,
833
+ newName
834
+ ]);
835
+ };
836
+ /**
837
+ * 移除远程
838
+ * @defaults `git remote remove <name>`
839
+ */
840
+ const gitRemoteRemove = async (name) => {
841
+ await gitRemote(["remove", name]);
842
+ };
843
+ /** 获取关联的所有远程 */
844
+ const getRemoteNames = async () => {
845
+ const res = await gitRemote([]);
846
+ return res ? splitNonEmptyLines(res) : [];
847
+ };
848
+ /** 获取远程地址 */
849
+ const getRemoteUrl = async (remote) => {
850
+ return gitRemoteGetUrl([remote]);
851
+ };
852
+ /** 获取所有远程 */
853
+ const getRemoteList = async () => {
854
+ const remotes = await getRemoteNames();
855
+ return Promise.all(remotes.map(async (name) => {
856
+ return {
857
+ name,
858
+ url: await gitRemoteGetUrl([name])
859
+ };
860
+ }));
861
+ };
862
+ // src/undo.revert.ts
863
+ /**
864
+ * 安全撤销一个已提交(并可能已 push)的 commit
865
+ * @defaults `git revert <commit>`
866
+ */
867
+ const gitRevertCommit = async (hash) => {
868
+ await gitRevert([hash]);
869
+ };
870
+ // src/undo.reset.ts
871
+ const headArg = (count = 1) => {
872
+ if (count < 0) throw new RangeError("count must be >= 0");
873
+ return count === 0 ? "HEAD" : `HEAD~${count}`;
874
+ };
875
+ /**
876
+ * 撤销最近的 commit(移动 HEAD + 取消暂存)
877
+ * - 暂存区被重置到目标 commit
878
+ * - 未提交修改保持不变
879
+ * @defaults `git reset --mixed HEAD~<count>`
880
+ */
881
+ const gitResetMixed = async (count = 1) => {
882
+ await gitReset(["--mixed", headArg(count)]);
883
+ };
884
+ /**
885
+ * 撤销最近的 commit(完全重置)
886
+ * - 暂存区 + 工作区都会被重置到目标 commit
887
+ * - 未提交修改会丢失
888
+ * @defaults `git reset --hard HEAD~<count>`
889
+ */
890
+ const gitResetHard = async (count = 1) => {
891
+ await gitReset(["--hard", headArg(count)]);
892
+ };
893
+ /**
894
+ * 撤销最近的 commit(仅移动 HEAD)
895
+ * - 暂存区保持不变
896
+ * - 未提交修改保持不变
897
+ * @defaults `git reset --soft HEAD~<count>`
898
+ */
899
+ const gitResetSoft = async (count = 1) => {
900
+ await gitReset(["--soft", headArg(count)]);
901
+ };
902
+ /**
903
+ * 撤销最近的 commit(安全重置)
904
+ * - 行为类似 --hard
905
+ * - 若会覆盖工作区未提交修改则直接失败
906
+ * @defaults `git reset --keep HEAD~<count>`
907
+ */
908
+ const gitResetKeep = async (count = 1) => {
909
+ await gitReset(["--keep", headArg(count)]);
910
+ };
911
+ /** {@link gitResetMixed} 的同步版本 */
912
+ const gitResetMixedSync = (count = 1) => {
913
+ gitResetSync(["--mixed", headArg(count)]);
914
+ };
915
+ /** {@link gitResetHard} 的同步版本 */
916
+ const gitResetHardSync = (count = 1) => {
917
+ gitResetSync(["--hard", headArg(count)]);
918
+ };
919
+ /** {@link gitResetSoft} 的同步版本 */
920
+ const gitResetSoftSync = (count = 1) => {
921
+ gitResetSync(["--soft", headArg(count)]);
922
+ };
923
+ /** {@link gitResetKeep} 的同步版本 */
924
+ const gitResetKeepSync = (count = 1) => {
925
+ gitResetSync(["--keep", headArg(count)]);
926
+ };
927
+ // src/undo.restore.ts
928
+ /**
929
+ * 用【暂存区】覆盖【工作区】单个文件
930
+ * - 用 Index 覆盖 Working Tree
931
+ * - 不影响暂存区(Index)
932
+ * @defaults `git restore --worktree <file>`
933
+ */
934
+ const gitRestoreFile = async (file) => {
935
+ await gitRestore(["--worktree", file]);
936
+ };
937
+ /**
938
+ * 用【暂存区】覆盖【工作区】所有文件
939
+ * - 用 Index 覆盖 Working Tree
940
+ * - 不影响暂存区(Index)
941
+ * @defaults `git --worktree restore .`
942
+ */
943
+ const gitRestoreAll = async () => {
944
+ await gitRestore(["--worktree", "."]);
945
+ };
946
+ /**
947
+ * 用 指定 source 中的文件内容覆盖【工作区】文件
948
+ * - 仅修改 Working Tree
949
+ * - 不影响暂存区(Index)
950
+ * - 不会改变提交历史(HEAD)
951
+ * @defaults `git restore --worktree --source <source> <file>`
952
+ * @see source = `git rev-parse <expr>`
953
+ */
954
+ const gitRestoreWorktreeFileFrom = async (source, file) => {
955
+ await gitRestore([
956
+ "--worktree",
957
+ "--source",
958
+ source,
959
+ file
960
+ ]);
961
+ };
962
+ /**
963
+ * 用指定 source 中的文件内容覆盖【工作区】所有文件
964
+ * - 仅修改 Working Tree
965
+ * - 不影响暂存区(Index)
966
+ * - 不会改变提交历史(HEAD)
967
+ * @defaults `git restore --worktree --source <source> .`
968
+ * @see source = `git rev-parse <expr>`
969
+ */
970
+ const gitRestoreWorktreeAllFrom = async (source) => {
971
+ await gitRestore([
972
+ "--worktree",
973
+ "--source",
974
+ source,
975
+ "."
976
+ ]);
977
+ };
978
+ /**
979
+ * 用指定 source 覆盖【暂存区】中的文件
980
+ * - 用 source 覆盖 Index
981
+ * - 不影响工作区(Working Tree)
982
+ * - 不会改变提交历史(HEAD)
983
+ * @defaults `git restore --staged --source <source> <file>`
984
+ * @see source = `git rev-parse <expr>`
985
+ */
986
+ const gitRestoreIndexFileFrom = async (source, file) => {
987
+ await gitRestore([
988
+ `--staged`,
989
+ "--source",
990
+ source,
991
+ file
992
+ ]);
993
+ };
994
+ /**
995
+ * 用指定 source 覆盖整个【暂存区】
996
+ * - 用 source 覆盖 Index
997
+ * - 不影响工作区(Working Tree)
998
+ * - 不会改变提交历史(HEAD)
999
+ * @defaults `git restore --staged --source <source> .`
1000
+ * @see source = `git rev-parse <expr>`
1001
+ */
1002
+ const gitRestoreIndexAllFrom = async (source) => {
1003
+ await gitRestore([
1004
+ `--staged`,
1005
+ "--source",
1006
+ source,
1007
+ "."
1008
+ ]);
1009
+ };
1010
+ /**
1011
+ * 用指定 source 覆盖指定文件(暂存区 + 工作区)
1012
+ * - 将 Index 和 Working Tree 恢复到 source 状态
1013
+ * - 不会移动 HEAD
1014
+ * - 不会改变提交历史
1015
+ * @defaults `git restore --staged --worktree --source <source> <file>`
1016
+ * @see source = `git rev-parse <expr>`
1017
+ */
1018
+ const gitRestoreFileFrom = async (source, file) => {
1019
+ await gitRestore([
1020
+ "--staged",
1021
+ "--worktree",
1022
+ "--source",
1023
+ source,
1024
+ file
1025
+ ]);
1026
+ };
1027
+ /**
1028
+ * 用指定 source 覆盖所有文件(暂存区 + 工作区)
1029
+ * - 将 Index 和 Working Tree 恢复到 source 状态
1030
+ * - 不会移动 HEAD
1031
+ * - 不会改变提交历史
1032
+ * @defaults `git restore --staged --worktree --source <source> .`
1033
+ * @see source = `git rev-parse <expr>`
1034
+ */
1035
+ const gitRestoreAllFrom = async (source) => {
1036
+ await gitRestore([
1037
+ "--staged",
1038
+ "--worktree",
1039
+ "--source",
1040
+ source,
1041
+ "."
1042
+ ]);
1043
+ };
1044
+ // src/undo.unstaged.ts
1045
+ /**
1046
+ * 取消单个文件的暂存状态
1047
+ * - 用 HEAD 覆盖暂存区(Index)
1048
+ * - 不影响工作区(Working Tree)
1049
+ * - 等价于撤销 git add
1050
+ * @defaults `git restore --staged <file>`
1051
+ */
1052
+ const gitUnstageFile = async (file) => {
1053
+ await gitRestore(["--staged", file]);
1054
+ };
1055
+ /**
1056
+ * 取消所有文件的暂存状态
1057
+ * - 用 HEAD 覆盖暂存区(Index)
1058
+ * - 不影响工作区(Working Tree)
1059
+ * - 等价于撤销所有 git add
1060
+ * @defaults `git restore --staged .`
1061
+ */
1062
+ const gitUnstageAll = async () => {
1063
+ await gitRestore(["--staged", "."]);
1064
+ };
1065
+ // src/undo.discard.ts
1066
+ /**
1067
+ * 丢弃指定文件的所有修改(暂存区 + 工作区)
1068
+ * - 强制将文件恢复到 HEAD 状态
1069
+ * - 同时重置 Index 和 Working Tree
1070
+ * - 不会改变提交历史(HEAD)
1071
+ * @defaults `git restore --source=HEAD --staged --worktree <file>`
1072
+ */
1073
+ const gitDiscardFile = async (file) => {
1074
+ await gitRestore([
1075
+ "--staged",
1076
+ "--worktree",
1077
+ "--source=HEAD",
1078
+ file
1079
+ ]);
1080
+ };
1081
+ /**
1082
+ * 丢弃所有文件的所有修改(暂存区 + 工作区)
1083
+ * - 强制将整个工作区恢复到 HEAD 状态
1084
+ * - 行为等价于 git reset --hard HEAD(按文件粒度)
1085
+ * - 不会删除未跟踪文件(untracked files)
1086
+ * @defaults `git restore --source=HEAD --staged --worktree .`
1087
+ */
1088
+ const gitDiscardAll = async () => {
1089
+ await gitRestore([
1090
+ "--staged",
1091
+ "--worktree",
1092
+ "--source=HEAD",
1093
+ "."
1094
+ ]);
1095
+ };
1096
+ export { GitSorter, coloredStatus, countCommitsSince, createUnbornBranch, fetchAllPrune, getFullHash, getLatestTag, getLocalBranches, getLocalTags, getLogSince, getPreviousTag, getRemoteBranches, getRemoteList, getRemoteNames, getRemoteTags, getRemoteUrl, getShortHash, getShortStatus, gitAdd, gitAddAll, gitAddSync, gitAddTracked, gitBranch, gitBranchCurrent, gitBranchDelete, gitBranchDeleteForce, gitBranchMove, gitBranchMoveForce, gitBranchSync, gitCatFile, gitCatFileSync, gitCheckout, gitCheckoutBranch, gitCheckoutBranchForce, gitCheckoutBranchOrphan, gitCheckoutSync, gitCommit, gitCommitAllowEmpty, gitCommitAmend, gitCommitAmendNoEdit, gitCommitMessage, gitCommitSync, gitConfig, gitConfigGet, gitConfigList, gitConfigSet, gitConfigSync, gitConfigUnset, gitDeleteHeadRef, gitDiff, gitDiffSync, gitDiscardAll, gitDiscardFile, gitFetch, gitFetchSync, gitForEachRef, gitForEachRefSync, gitInit, gitInitSync, gitLog, gitLogSync, gitLsRemote, gitLsRemoteSync, gitMv, gitMvSync, gitPush, gitPushBranch, gitPushDeleteRef, gitPushSync, gitPushTag, gitRemote, gitRemoteAdd, gitRemoteGetUrl, gitRemoteRemove, gitRemoteRename, gitRemoteSetUrl, gitRemoteSync, gitReset, gitResetHard, gitResetHardSync, gitResetKeep, gitResetKeepSync, gitResetMixed, gitResetMixedSync, gitResetSoft, gitResetSoftSync, gitResetSync, gitRestore, gitRestoreAll, gitRestoreAllFrom, gitRestoreFile, gitRestoreFileFrom, gitRestoreIndexAllFrom, gitRestoreIndexFileFrom, gitRestoreSync, gitRestoreWorktreeAllFrom, gitRestoreWorktreeFileFrom, gitRevList, gitRevListSync, gitRevParse, gitRevParseSync, gitRevert, gitRevertCommit, gitRevertSync, gitRm, gitRmSync, gitSetHeadBranch, gitShow, gitShowSync, gitStash, gitStashSync, gitStatus, gitStatusSync, gitSymbolicRef, gitSymbolicRefSync, gitTag, gitTagAnnotated, gitTagDelete, gitTagDeleteSync, gitTagLightweight, gitTagSigned, gitTagSync, gitUnstageAll, gitUnstageFile, gitUpdateRef, gitUpdateRefSync, hasUpstream, headArg, initBareRepo, initRepo, isGitBareRepo, isGitRepo, isRemoteName, isWorkingTreeClean, resolveChangelogRange };