@tertium/gith 0.2.0 → 0.3.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/README.md +10 -1
- package/bin/gith.js +310 -83
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,10 +35,16 @@ gith create -r 1.2.0 # release/1.2.0 from develop
|
|
|
35
35
|
gith create -h 1.2.1 # hotfix/1.2.1 from main
|
|
36
36
|
gith create --release # release, version = minor after the latest tag
|
|
37
37
|
gith create --hotfix # hotfix, version = patch after the latest tag
|
|
38
|
+
gith switch # list local and origin branches, and switch to the one you pick
|
|
38
39
|
gith switch -b login-redirect # switch to the branch, creating it from develop if it is missing
|
|
39
40
|
gith switch -commit 3f2a1b9 # detach HEAD at that commit, to look at old code
|
|
41
|
+
gith switch --release # switch to the newest release/* (local or origin); -r 1.9.0 for one version
|
|
42
|
+
gith switch --hotfix # switch to the newest hotfix/* (local or origin); -h 1.2.1 for one version
|
|
40
43
|
gith update # merge origin/develop into this work branch
|
|
41
44
|
gith merge # this branch into develop
|
|
45
|
+
gith finish --current-release # release the newest release/*: version, changelog, merge to main, tag, merge main to develop, then ask before deleting the branch
|
|
46
|
+
gith finish --current-hotfix # the same for the newest hotfix/*
|
|
47
|
+
gith merge # pick the branch to merge, then the target from the list
|
|
42
48
|
gith merge -r 1.2.0 # this branch into release/1.2.0
|
|
43
49
|
gith merge -h 1.2.1 # this branch into hotfix/1.2.1
|
|
44
50
|
gith merge --current-release # this branch into the newest release/* (local or origin)
|
|
@@ -59,6 +65,7 @@ gith reset # discard uncommitted changes, then clean
|
|
|
59
65
|
gith clean # remove untracked files and directories
|
|
60
66
|
|
|
61
67
|
# Clean up branches (every removal shows what it will remove first)
|
|
68
|
+
gith remove # pick branches to delete from the list (here and on origin)
|
|
62
69
|
gith remove -b login-redirect # delete the branch here and on origin (rm is an alias)
|
|
63
70
|
gith remove -b login-redirect --local # delete only the local branch
|
|
64
71
|
gith remove -b login-redirect --remote # delete only the origin branch
|
|
@@ -141,7 +148,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
141
148
|
- **`switch -b`** switches to a branch. If the branch exists neither locally nor on origin, it is created first, the same way as `create -b`, and then checked out.
|
|
142
149
|
- **`switch -commit <sha>`** detaches HEAD at that commit so you can look at old code. It refuses a commit that does not exist, and it refuses while tracked files have changes (untracked files do not block it). It lists the files that block it, and points to `gith stash`. On a detached HEAD, `gith switch -b <name>` creates the branch at the current commit, so commits made there stay on it. It does not create or switch branches for other commits, so commits made on a detached HEAD are easy to lose: name a branch before you leave.
|
|
143
150
|
- **`remove -b <branch>`** removes the branch on both sides: the local branch first (with `git branch -d`, so git refuses to delete an unmerged branch), then the origin branch. A side that does not exist is skipped, so a branch that lives only here is still removed. A name that exists in neither place is an error. `--local` touches only this clone; `--remote` touches only origin. Containers (`develop`, `main`, `release/*`, `hotfix/*`) are refused.
|
|
144
|
-
- **`remove
|
|
151
|
+
- **`remove`** with no name opens the list of branches and asks which to remove, the same as `remove --branch`. By default it lists branch pairs, meaning branches that exist both here and on origin. `--local` lists local branches and `--remote` lists origin branches. Each choice is removed on the same sides as above. `-limit <n>` shows only the first `n` (all by default), and the picker numbers only those. Pick with the arrow keys: up and down move, space checks an item, enter confirms the checked items, and esc cancels. `switch` takes one item with enter. The list needs a terminal, and that is checked before anything is listed. The checked-out branch and containers are never listed. Choosing items is the confirmation, so nothing else is asked. If a delete fails (for example, an unmerged branch), the removal stops there and earlier picks stay deleted.
|
|
145
152
|
- **`status`** prints the branch and how far it is ahead of or behind its origin branch, then counts staged, unstaged, untracked and conflicting files. It changes nothing.
|
|
146
153
|
- **`update`** fetches `origin/develop` and merges it into the current work branch. It needs a clean working tree and refuses containers. If the merge conflicts, it aborts the merge and says so. It does not push.
|
|
147
154
|
- **`undo`** runs `git reset --soft HEAD~1`: the last commit is removed and its changes stay staged. It refuses when HEAD is already on origin, because undoing a pushed commit needs a force-push.
|
|
@@ -176,6 +183,8 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
176
183
|
- **`create -r` / `create -h`** creates a container from its base (`develop` for release, `main` for hotfix), refusing names that already exist on origin. Like `create -b`, it does not switch.
|
|
177
184
|
- **With `--release` or `--hotfix` (no value)**, the version is worked out from the highest `vX.Y.Z` tag across local tags and origin: `-r` takes the next minor (`1.2.1` → `1.3.0`), `-h` takes the next patch (`1.2.1` → `1.2.2`). With no tags yet, the first release is `0.1.0` and the first hotfix `0.0.1`. The version is printed before the branch is created.
|
|
178
185
|
- **`merge --current-release`** and **`--current-hotfix`** pick the newest `release/*` or `hotfix/*` branch by version (numbers compare as numbers, so `1.10.0` beats `1.9.0`), checking both local branches and origin, and merge into it. The target must exist on origin, because the merge pushes it; a newest branch that only exists here stops the command with a message.
|
|
186
|
+
- **`finish --current-release`** (and `--current-hotfix`) releases the newest `release/*` (or `hotfix/*`), named by its version X.Y.Z. It makes `package.json`'s version match the name (committed as `X.Y.Z`, only when they differ), adds the changelog entry as its own commit (when it is missing), merges the branch into `main` with `--no-ff`, tags `vX.Y.Z` and pushes, merges `main` into `develop` when `develop` exists on origin, and then asks before deleting the branch. A conflict aborts the merge and stops. Projects without a `package.json` keep their version by hand.
|
|
187
|
+
- **`merge`** with no flags asks which branch to merge and which target to merge it into (develop, release/*, hotfix/*), with the arrow keys. After a merge into a target, the work branch is deleted here and on origin, after a Y/n question. Without a terminal, or on a no, it is kept. Release and hotfix branches are never deleted this way.
|
|
179
188
|
- **`merge`** pushes the work branch, merges the target into it first (so conflicts happen on the work branch and are aborted cleanly), then merges it into the target with `--no-ff`, pushes, and checks that both branches match origin.
|
|
180
189
|
|
|
181
190
|
Containers (`develop`, `main`, `release/*`, `hotfix/*`) can't be merged from, and only `develop`, `release/*` and `hotfix/*` can be merged into.
|
package/bin/gith.js
CHANGED
|
@@ -28,6 +28,7 @@ var SPECS = new Map([
|
|
|
28
28
|
["stashes", "switch"],
|
|
29
29
|
["current-release", "switch"],
|
|
30
30
|
["current-hotfix", "switch"],
|
|
31
|
+
["develop", "switch"],
|
|
31
32
|
["pockets", "switch"],
|
|
32
33
|
["detached", "switch"],
|
|
33
34
|
["all", "switch"],
|
|
@@ -427,6 +428,124 @@ function find(flags) {
|
|
|
427
428
|
`));
|
|
428
429
|
}
|
|
429
430
|
|
|
431
|
+
// src/components/flow/flow.ts
|
|
432
|
+
import { execSync } from "node:child_process";
|
|
433
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
434
|
+
|
|
435
|
+
// src/components/prompt/prompt.ts
|
|
436
|
+
import { emitKeypressEvents } from "node:readline";
|
|
437
|
+
import { createInterface } from "node:readline/promises";
|
|
438
|
+
|
|
439
|
+
// src/components/prompt/prompt.utils.ts
|
|
440
|
+
function moveIndex(index, delta, length) {
|
|
441
|
+
if (length <= 0)
|
|
442
|
+
return 0;
|
|
443
|
+
return ((index + delta) % length + length) % length;
|
|
444
|
+
}
|
|
445
|
+
function toggled(checked, index) {
|
|
446
|
+
const next = new Set(checked);
|
|
447
|
+
if (next.has(index))
|
|
448
|
+
next.delete(index);
|
|
449
|
+
else
|
|
450
|
+
next.add(index);
|
|
451
|
+
return next;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/components/prompt/prompt.ts
|
|
455
|
+
var assumeYes = false;
|
|
456
|
+
function setAssumeYes(on) {
|
|
457
|
+
assumeYes = on;
|
|
458
|
+
}
|
|
459
|
+
async function confirm(question) {
|
|
460
|
+
if (isDryRun()) {
|
|
461
|
+
console.log(`dry run: would ask: ${question}`);
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
if (assumeYes)
|
|
465
|
+
return true;
|
|
466
|
+
if (!process.stdin.isTTY)
|
|
467
|
+
throw new Error("this needs a terminal to confirm; pass --yes to skip the question.");
|
|
468
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
469
|
+
try {
|
|
470
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
471
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
472
|
+
} finally {
|
|
473
|
+
rl.close();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function ask(question) {
|
|
477
|
+
if (isDryRun()) {
|
|
478
|
+
console.log(`dry run: would ask: ${question}`);
|
|
479
|
+
return "<message>";
|
|
480
|
+
}
|
|
481
|
+
if (!process.stdin.isTTY)
|
|
482
|
+
throw new Error("needs a terminal to ask; pass -m <message>.");
|
|
483
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
484
|
+
try {
|
|
485
|
+
return (await rl.question(question)).trim();
|
|
486
|
+
} finally {
|
|
487
|
+
rl.close();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async function select(question, options, multi) {
|
|
491
|
+
if (!process.stdin.isTTY)
|
|
492
|
+
throw new Error("this needs a terminal to pick.");
|
|
493
|
+
return new Promise((resolve) => {
|
|
494
|
+
const stdin = process.stdin;
|
|
495
|
+
let index = 0;
|
|
496
|
+
let checked = new Set;
|
|
497
|
+
let drawnLines = 0;
|
|
498
|
+
const draw = () => {
|
|
499
|
+
if (drawnLines > 0)
|
|
500
|
+
process.stdout.write(`\x1B[${drawnLines}A\x1B[0J`);
|
|
501
|
+
const hint = multi ? "space checks, enter confirms, esc cancels" : "enter selects, esc cancels";
|
|
502
|
+
const lines = [`${question} (${hint})`];
|
|
503
|
+
options.forEach((option, i) => {
|
|
504
|
+
const cursor = i === index ? ">" : " ";
|
|
505
|
+
const mark = multi ? checked.has(i) ? "[x]" : "[ ]" : "";
|
|
506
|
+
lines.push(`${cursor} ${mark} ${option.label}`.replace(/ +$/, ""));
|
|
507
|
+
});
|
|
508
|
+
process.stdout.write(`${lines.join(`
|
|
509
|
+
`)}
|
|
510
|
+
`);
|
|
511
|
+
drawnLines = lines.length;
|
|
512
|
+
};
|
|
513
|
+
const finish = (picked) => {
|
|
514
|
+
stdin.off("keypress", onKey);
|
|
515
|
+
stdin.setRawMode(false);
|
|
516
|
+
stdin.pause();
|
|
517
|
+
process.stdout.write(`
|
|
518
|
+
`);
|
|
519
|
+
resolve(picked);
|
|
520
|
+
};
|
|
521
|
+
const onKey = (_text, key) => {
|
|
522
|
+
if (!key)
|
|
523
|
+
return;
|
|
524
|
+
if (key.ctrl && key.name === "c" || key.name === "escape") {
|
|
525
|
+
finish([]);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (key.name === "return") {
|
|
529
|
+
const picked = multi ? [...checked].sort((a, b) => a - b).map((i) => options[i]?.name ?? "") : [options[index]?.name ?? ""];
|
|
530
|
+
finish(picked.filter(Boolean));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (key.name === "up")
|
|
534
|
+
index = moveIndex(index, -1, options.length);
|
|
535
|
+
else if (key.name === "down")
|
|
536
|
+
index = moveIndex(index, 1, options.length);
|
|
537
|
+
else if (key.name === "space" && multi)
|
|
538
|
+
checked = toggled(checked, index);
|
|
539
|
+
draw();
|
|
540
|
+
};
|
|
541
|
+
emitKeypressEvents(stdin);
|
|
542
|
+
stdin.setRawMode(true);
|
|
543
|
+
stdin.resume();
|
|
544
|
+
stdin.on("keypress", onKey);
|
|
545
|
+
draw();
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
430
549
|
// src/components/rules/rules.constants.ts
|
|
431
550
|
var CONTAINER = /^(develop|main|release\/.+|hotfix\/.+)$/;
|
|
432
551
|
var TARGET = /^(develop|release\/.+|hotfix\/.+)$/;
|
|
@@ -534,15 +653,20 @@ function create(flags) {
|
|
|
534
653
|
}
|
|
535
654
|
open(kind, value || nextContainerVersion(kind));
|
|
536
655
|
}
|
|
537
|
-
function merge(flags) {
|
|
656
|
+
async function merge(flags) {
|
|
657
|
+
if (flags.size === 0)
|
|
658
|
+
return mergePick();
|
|
538
659
|
const kind = onlyOne(flags, [
|
|
660
|
+
"develop",
|
|
539
661
|
"release",
|
|
540
662
|
"hotfix",
|
|
541
663
|
"current-release",
|
|
542
664
|
"current-hotfix"
|
|
543
665
|
]);
|
|
544
666
|
let target = "develop";
|
|
545
|
-
if (kind === "
|
|
667
|
+
if (kind === "develop")
|
|
668
|
+
target = "develop";
|
|
669
|
+
else if (kind === "current-release")
|
|
546
670
|
target = newestContainer("release");
|
|
547
671
|
else if (kind === "current-hotfix")
|
|
548
672
|
target = newestContainer("hotfix");
|
|
@@ -553,7 +677,7 @@ function merge(flags) {
|
|
|
553
677
|
}
|
|
554
678
|
target = containerName(kind, version);
|
|
555
679
|
}
|
|
556
|
-
mergeInto(target);
|
|
680
|
+
await mergeInto(target);
|
|
557
681
|
}
|
|
558
682
|
function createBranch(goal, from) {
|
|
559
683
|
if (isContainer(goal))
|
|
@@ -568,10 +692,11 @@ function createBranch(goal, from) {
|
|
|
568
692
|
}
|
|
569
693
|
gitArgs(["push", "-u", "origin", goal]);
|
|
570
694
|
}
|
|
571
|
-
function mergeInto(target) {
|
|
695
|
+
async function mergeInto(target, from) {
|
|
572
696
|
if (!isTarget(target))
|
|
573
697
|
throw new Error(`'${target}' is not a valid target.`);
|
|
574
|
-
const
|
|
698
|
+
const current = capture("rev-parse --abbrev-ref HEAD");
|
|
699
|
+
const source = from ?? current;
|
|
575
700
|
if (isContainer(source))
|
|
576
701
|
throw new Error(`'${source}' is a container and cannot be merged from.`);
|
|
577
702
|
if (source === target)
|
|
@@ -579,6 +704,8 @@ function mergeInto(target) {
|
|
|
579
704
|
if (!hasOnOrigin(target))
|
|
580
705
|
throw new Error(`'${target}' does not exist on origin.`);
|
|
581
706
|
assertCleanTree();
|
|
707
|
+
if (source !== current)
|
|
708
|
+
gitArgs(["switch", "--", source]);
|
|
582
709
|
if (hasOnOrigin(source))
|
|
583
710
|
git(`pull origin ${source}`);
|
|
584
711
|
git(`push -u origin ${source}`);
|
|
@@ -598,44 +725,116 @@ function mergeInto(target) {
|
|
|
598
725
|
assertSynced([target, source]);
|
|
599
726
|
console.log(`
|
|
600
727
|
✅ '${source}' merged into '${target}', both in sync with origin.`);
|
|
728
|
+
await deleteMergedBranch(source);
|
|
601
729
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
function setAssumeYes(on) {
|
|
607
|
-
assumeYes = on;
|
|
608
|
-
}
|
|
609
|
-
async function confirm(question) {
|
|
610
|
-
if (isDryRun()) {
|
|
611
|
-
console.log(`dry run: would ask: ${question}`);
|
|
612
|
-
return true;
|
|
613
|
-
}
|
|
614
|
-
if (assumeYes)
|
|
615
|
-
return true;
|
|
616
|
-
if (!process.stdin.isTTY)
|
|
617
|
-
throw new Error("this needs a terminal to confirm; pass --yes to skip the question.");
|
|
618
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
730
|
+
async function deleteMergedBranch(source) {
|
|
731
|
+
const local = hasLocal(source);
|
|
732
|
+
const origin = hasOnOrigin(source);
|
|
733
|
+
let yes;
|
|
619
734
|
try {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
rl.close();
|
|
735
|
+
yes = await confirm(`Delete '${source}' here and on origin, it is merged? [Y/n] `);
|
|
736
|
+
} catch {
|
|
737
|
+
yes = false;
|
|
624
738
|
}
|
|
739
|
+
if (!yes) {
|
|
740
|
+
console.log(`'${source}' kept.`);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (local)
|
|
744
|
+
gitArgs(["branch", "-d", "--", source]);
|
|
745
|
+
if (origin)
|
|
746
|
+
gitArgs(["push", "origin", "--delete", source]);
|
|
625
747
|
}
|
|
626
|
-
async function
|
|
748
|
+
async function mergePick() {
|
|
749
|
+
if (!process.stdin.isTTY) {
|
|
750
|
+
throw new Error("pass -r <version> or -h <version> instead; the pickers need a terminal");
|
|
751
|
+
}
|
|
752
|
+
const current = capture("rev-parse --abbrev-ref HEAD");
|
|
753
|
+
const local = captureArgs([
|
|
754
|
+
"for-each-ref",
|
|
755
|
+
"--format=%(refname:short)",
|
|
756
|
+
"refs/heads"
|
|
757
|
+
]).split(`
|
|
758
|
+
`).filter(Boolean);
|
|
759
|
+
const sources = local.filter((name) => !isContainer(name)).sort((a, b) => a.localeCompare(b)).map((name) => ({
|
|
760
|
+
name,
|
|
761
|
+
label: name === current ? `${name} (current)` : name
|
|
762
|
+
}));
|
|
763
|
+
if (sources.length === 0)
|
|
764
|
+
throw new Error("no work branch to merge.");
|
|
765
|
+
const [source] = await select("Which branch to merge?", sources, false);
|
|
766
|
+
if (source === undefined)
|
|
767
|
+
return;
|
|
768
|
+
const origin = captureArgs(["ls-remote", "--heads", "origin"]).split(`
|
|
769
|
+
`).map((line) => (line.split("\t")[1] ?? "").replace(/^refs\/heads\//, "")).filter(Boolean);
|
|
770
|
+
const targets = [...new Set([...local, ...origin])].filter((name) => isTarget(name) && name !== source).sort((a, b) => a.localeCompare(b)).map((name) => ({
|
|
771
|
+
name,
|
|
772
|
+
label: local.includes(name) ? name : `${name} (origin)`
|
|
773
|
+
}));
|
|
774
|
+
if (targets.length === 0)
|
|
775
|
+
throw new Error("no develop, release or hotfix branch to merge into.");
|
|
776
|
+
const [target] = await select(`Merge '${source}' into which?`, targets, false);
|
|
777
|
+
if (target === undefined)
|
|
778
|
+
return;
|
|
779
|
+
await mergeInto(target, source);
|
|
780
|
+
}
|
|
781
|
+
function alignVersion(version) {
|
|
782
|
+
if (!existsSync2("package.json"))
|
|
783
|
+
return;
|
|
784
|
+
const current = parsePackageVersion(readFileSync2("package.json", "utf8"));
|
|
785
|
+
if (current === version)
|
|
786
|
+
return;
|
|
627
787
|
if (isDryRun()) {
|
|
628
|
-
console.log(
|
|
629
|
-
|
|
788
|
+
console.log(`> npm version ${version} --no-git-tag-version --ignore-scripts`);
|
|
789
|
+
} else {
|
|
790
|
+
execSync(`npm version ${version} --no-git-tag-version --ignore-scripts`, {
|
|
791
|
+
stdio: "inherit"
|
|
792
|
+
});
|
|
630
793
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
794
|
+
gitArgs(["add", "--", "package.json"]);
|
|
795
|
+
gitArgs(["commit", "-m", version, "--", "package.json"]);
|
|
796
|
+
}
|
|
797
|
+
function addChangelogEntry(version) {
|
|
798
|
+
if (!existsSync2("package.json"))
|
|
799
|
+
return;
|
|
800
|
+
const file = "CHANGELOG.md";
|
|
801
|
+
if (existsSync2(file) && readFileSync2(file, "utf8").includes(`## ${version} - `))
|
|
802
|
+
return;
|
|
803
|
+
changelog(new Map);
|
|
804
|
+
}
|
|
805
|
+
async function finish(flags) {
|
|
806
|
+
const kind = onlyOne(flags, ["current-release", "current-hotfix"]);
|
|
807
|
+
if (!kind || flags.size > 1)
|
|
808
|
+
throw new Error("usage: gith finish --current-release | --current-hotfix");
|
|
809
|
+
const containerKind = kind === "current-release" ? "release" : "hotfix";
|
|
810
|
+
const branch = newestContainer(containerKind);
|
|
811
|
+
const version = branch.slice(containerKind.length + 1);
|
|
812
|
+
if (parseSemver(version) === undefined)
|
|
813
|
+
throw new Error(`'${branch}' is not named with a version (X.Y.Z).`);
|
|
814
|
+
assertCleanTree();
|
|
815
|
+
gitArgs(["switch", "--", branch]);
|
|
816
|
+
gitArgs(["pull", "--ff-only", "origin", branch]);
|
|
817
|
+
alignVersion(version);
|
|
818
|
+
addChangelogEntry(version);
|
|
819
|
+
gitArgs(["switch", "--", "main"]);
|
|
820
|
+
gitArgs(["pull", "--ff-only", "origin", "main"]);
|
|
634
821
|
try {
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
|
|
822
|
+
gitArgs(["merge", "--no-ff", branch, "-m", `Merge '${branch}' into main`]);
|
|
823
|
+
} catch {
|
|
824
|
+
gitArgs(["merge", "--abort"]);
|
|
825
|
+
throw new Error(`merging '${branch}' into main conflicts; resolve it by hand, then retry.`);
|
|
826
|
+
}
|
|
827
|
+
gitArgs(["tag", "-a", `v${version}`, "-m", `v${version}`]);
|
|
828
|
+
gitArgs(["push", "origin", "main"]);
|
|
829
|
+
gitArgs(["push", "origin", `v${version}`]);
|
|
830
|
+
if (hasOnOrigin("develop")) {
|
|
831
|
+
gitArgs(["switch", "--", "develop"]);
|
|
832
|
+
gitArgs(["pull", "--ff-only", "origin", "develop"]);
|
|
833
|
+
gitArgs(["merge", "--no-edit", "main"]);
|
|
834
|
+
gitArgs(["push", "origin", "develop"]);
|
|
638
835
|
}
|
|
836
|
+
gitArgs(["switch", "--", "main"]);
|
|
837
|
+
await deleteMergedBranch(branch);
|
|
639
838
|
}
|
|
640
839
|
|
|
641
840
|
// src/components/history/history.utils.ts
|
|
@@ -862,7 +1061,7 @@ function stashes(limit, withGorlum) {
|
|
|
862
1061
|
]).split(`
|
|
863
1062
|
`).filter(Boolean);
|
|
864
1063
|
if (rows.length === 0) {
|
|
865
|
-
console.log("No stashes.");
|
|
1064
|
+
console.log(withGorlum ? "There is nothing in my pockets!" : "No stashes.");
|
|
866
1065
|
return;
|
|
867
1066
|
}
|
|
868
1067
|
const shown = rows.slice(0, limit);
|
|
@@ -893,9 +1092,6 @@ function originTags() {
|
|
|
893
1092
|
`).filter(Boolean).map((line) => (line.split("\t")[1] ?? "").replace(/^refs\/tags\//, "")).filter((ref) => ref && !ref.endsWith("^{}"));
|
|
894
1093
|
}
|
|
895
1094
|
|
|
896
|
-
// src/components/maintenance/maintenance.ts
|
|
897
|
-
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
898
|
-
|
|
899
1095
|
// src/components/maintenance/maintenance.constants.ts
|
|
900
1096
|
var DEFAULT_DAYS = 90;
|
|
901
1097
|
|
|
@@ -944,7 +1140,21 @@ function parsePositive(flags, name) {
|
|
|
944
1140
|
}
|
|
945
1141
|
|
|
946
1142
|
// src/components/maintenance/maintenance.ts
|
|
947
|
-
function switchBranch(flags) {
|
|
1143
|
+
async function switchBranch(flags) {
|
|
1144
|
+
if (flags.size === 0)
|
|
1145
|
+
return switchPick();
|
|
1146
|
+
const kind = onlyOne(flags, ["release", "hotfix"]);
|
|
1147
|
+
if (kind !== undefined) {
|
|
1148
|
+
if (flags.size > 1)
|
|
1149
|
+
throw new Error(`usage: gith switch --${kind} | -${kind === "release" ? "r" : "h"} <version>`);
|
|
1150
|
+
const version = flags.get(kind) ?? "";
|
|
1151
|
+
const name = version === "" ? newestContainer(kind) : containerName(kind, version);
|
|
1152
|
+
if (!hasLocal(name) && !hasOnOrigin(name)) {
|
|
1153
|
+
throw new Error(`'${name}' does not exist, locally or on origin.`);
|
|
1154
|
+
}
|
|
1155
|
+
gitArgs(["switch", "--", name]);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
948
1158
|
const commit = flags.get("commit");
|
|
949
1159
|
if (commit !== undefined) {
|
|
950
1160
|
if (flags.has("branch") || flags.size > 1)
|
|
@@ -969,20 +1179,6 @@ function candidates(scope, local, remote, current) {
|
|
|
969
1179
|
}
|
|
970
1180
|
return local.filter((name) => notContainer(name) && notCurrent(name) && remote.includes(name)).map((name) => ({ name, label: `${name} (local + origin)` }));
|
|
971
1181
|
}
|
|
972
|
-
function parseSelection(answer, count) {
|
|
973
|
-
const indexes = new Set;
|
|
974
|
-
for (const part of answer.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
975
|
-
const range = /^(\d+)-(\d+)$/.exec(part);
|
|
976
|
-
const from = Number(range ? range[1] : part);
|
|
977
|
-
const to = Number(range ? range[2] : part);
|
|
978
|
-
if (!Number.isInteger(from) || !Number.isInteger(to) || from < 1 || to > count || from > to) {
|
|
979
|
-
throw new Error(`'${part}' is not a choice from 1 to ${count}.`);
|
|
980
|
-
}
|
|
981
|
-
for (let n = from;n <= to; n++)
|
|
982
|
-
indexes.add(n - 1);
|
|
983
|
-
}
|
|
984
|
-
return [...indexes];
|
|
985
|
-
}
|
|
986
1182
|
function branchTimes(output) {
|
|
987
1183
|
const times = new Map;
|
|
988
1184
|
for (const line of output.split(`
|
|
@@ -1028,7 +1224,7 @@ function detachedOptions(times, current) {
|
|
|
1028
1224
|
return options.sort((x, y) => x.name.localeCompare(y.name));
|
|
1029
1225
|
}
|
|
1030
1226
|
async function remove(flags) {
|
|
1031
|
-
const name = flags.get("branch");
|
|
1227
|
+
const name = flags.get("branch") ?? "";
|
|
1032
1228
|
const side = onlyOne(flags, ["local", "remote"]);
|
|
1033
1229
|
const scope = side ?? "pair";
|
|
1034
1230
|
const obsoleteOn = flags.has("obsolete");
|
|
@@ -1041,14 +1237,12 @@ async function remove(flags) {
|
|
|
1041
1237
|
throw new Error("--all only applies to --detached.");
|
|
1042
1238
|
if (obsoleteOn || detachedOn) {
|
|
1043
1239
|
const usage = detachedOn ? "usage: gith remove --detached [--all | -l <n>]" : "usage: gith remove --obsolete [--local] [-days <n>] [-l <n>]";
|
|
1044
|
-
if (
|
|
1240
|
+
if (flags.has("branch") || obsoleteOn && side === "remote" || detachedOn && side)
|
|
1045
1241
|
throw new Error(usage);
|
|
1046
1242
|
if (detachedOn)
|
|
1047
1243
|
return detached(flags);
|
|
1048
1244
|
return obsolete(flags, side === "local" ? "local" : "pair");
|
|
1049
1245
|
}
|
|
1050
|
-
if (name === undefined)
|
|
1051
|
-
throw new Error("usage: gith remove -b <branch> [--local | --remote], or gith remove --branch to pick");
|
|
1052
1246
|
if (name) {
|
|
1053
1247
|
if (flags.has("limit"))
|
|
1054
1248
|
throw new Error("-limit only applies to listings: gith remove --branch.");
|
|
@@ -1103,25 +1297,49 @@ async function clean(flags) {
|
|
|
1103
1297
|
}
|
|
1104
1298
|
gitArgs(["clean", "-fd"]);
|
|
1105
1299
|
}
|
|
1106
|
-
|
|
1300
|
+
var REMOVE_ACTION = {
|
|
1301
|
+
empty: "No branches to remove.",
|
|
1302
|
+
prompt: "Remove which?",
|
|
1303
|
+
multi: true
|
|
1304
|
+
};
|
|
1305
|
+
async function choose(options, limit, action = REMOVE_ACTION) {
|
|
1107
1306
|
if (options.length === 0) {
|
|
1108
|
-
console.log(
|
|
1307
|
+
console.log(action.empty);
|
|
1109
1308
|
return [];
|
|
1110
1309
|
}
|
|
1111
1310
|
const shown = limit === undefined ? options : options.slice(0, limit);
|
|
1112
|
-
for (const [i, option] of shown.entries()) {
|
|
1113
|
-
console.log(` ${i + 1}. ${option.label}`);
|
|
1114
|
-
}
|
|
1115
1311
|
if (shown.length < options.length) {
|
|
1116
1312
|
console.log(`showing ${shown.length} of ${options.length}; -limit <n> shows more`);
|
|
1117
1313
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1314
|
+
return select(action.prompt, shown, action.multi);
|
|
1315
|
+
}
|
|
1316
|
+
function switchOptions() {
|
|
1317
|
+
const local = localBranches();
|
|
1318
|
+
const origin = remoteBranches();
|
|
1319
|
+
const current = currentBranch2();
|
|
1320
|
+
const names = [...new Set([...local, ...origin])].sort((a, b) => a.localeCompare(b));
|
|
1321
|
+
return names.map((name) => {
|
|
1322
|
+
const where = local.includes(name) && origin.includes(name) ? "local + origin" : local.includes(name) ? "local" : "origin";
|
|
1323
|
+
return {
|
|
1324
|
+
name,
|
|
1325
|
+
label: `${name} (${where}${name === current ? ", current" : ""})`
|
|
1326
|
+
};
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
async function switchPick() {
|
|
1330
|
+
if (!process.stdin.isTTY) {
|
|
1331
|
+
throw new Error("pass a branch name with -b, e.g. gith switch -b <branch>; the list needs a terminal");
|
|
1124
1332
|
}
|
|
1333
|
+
const picked = await choose(switchOptions(), undefined, {
|
|
1334
|
+
empty: "No branches.",
|
|
1335
|
+
prompt: "Switch to which?",
|
|
1336
|
+
multi: false
|
|
1337
|
+
});
|
|
1338
|
+
if (picked.length === 0)
|
|
1339
|
+
return;
|
|
1340
|
+
if (picked.length > 1)
|
|
1341
|
+
throw new Error("pick exactly one branch to switch to.");
|
|
1342
|
+
gitArgs(["switch", "--", picked[0] ?? ""]);
|
|
1125
1343
|
}
|
|
1126
1344
|
function requireTerminal() {
|
|
1127
1345
|
if (!process.stdin.isTTY) {
|
|
@@ -1147,14 +1365,14 @@ async function removeBranches(scope, names, ask = true) {
|
|
|
1147
1365
|
});
|
|
1148
1366
|
if (plan.length > 0)
|
|
1149
1367
|
console.log("Will remove:");
|
|
1150
|
-
if (ask && plan.length > 0 && !await confirm(plan.length === 1 ? "Remove this branch? [Y/n] " : `Remove these ${plan.length} branches? [Y/n] `)) {
|
|
1151
|
-
console.log("Nothing removed.");
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
1368
|
for (const { name, local, origin } of plan) {
|
|
1155
1369
|
const sides = [local && "local", origin && "origin"].filter(Boolean).join(" + ");
|
|
1156
1370
|
console.log(` ${name} (${sides})`);
|
|
1157
1371
|
}
|
|
1372
|
+
if (ask && plan.length > 0 && !await confirm(plan.length === 1 ? "Remove this branch? [Y/n] " : `Remove these ${plan.length} branches? [Y/n] `)) {
|
|
1373
|
+
console.log("Nothing removed.");
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1158
1376
|
for (const { name, local, origin } of plan) {
|
|
1159
1377
|
if (local)
|
|
1160
1378
|
gitArgs(["branch", "-d", "--", name]);
|
|
@@ -1264,12 +1482,12 @@ function openerFor(platform, url) {
|
|
|
1264
1482
|
var KINDS = ["patch", "minor", "major"];
|
|
1265
1483
|
|
|
1266
1484
|
// src/components/release/release.utils.ts
|
|
1267
|
-
import { execSync } from "node:child_process";
|
|
1485
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
1268
1486
|
function npmVersion(kind) {
|
|
1269
1487
|
console.log(`> npm version ${kind}`);
|
|
1270
1488
|
if (isDryRun())
|
|
1271
1489
|
return;
|
|
1272
|
-
|
|
1490
|
+
execSync2(`npm version ${kind}`, { stdio: "inherit" });
|
|
1273
1491
|
}
|
|
1274
1492
|
|
|
1275
1493
|
// src/components/release/release.ts
|
|
@@ -1660,7 +1878,7 @@ function tree(flags) {
|
|
|
1660
1878
|
}
|
|
1661
1879
|
|
|
1662
1880
|
// src/components/version/version.ts
|
|
1663
|
-
import { readFileSync as
|
|
1881
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1664
1882
|
|
|
1665
1883
|
// src/components/version/version.constants.ts
|
|
1666
1884
|
var MASCOT = [
|
|
@@ -1675,15 +1893,15 @@ var MASCOT = [
|
|
|
1675
1893
|
];
|
|
1676
1894
|
|
|
1677
1895
|
// src/components/version/version.utils.ts
|
|
1678
|
-
import { existsSync as
|
|
1896
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1679
1897
|
import path2 from "node:path";
|
|
1680
1898
|
import { fileURLToPath } from "node:url";
|
|
1681
1899
|
function packageJsonPath() {
|
|
1682
1900
|
let dir = path2.dirname(fileURLToPath(import.meta.url));
|
|
1683
1901
|
for (;; ) {
|
|
1684
1902
|
const candidate = path2.join(dir, "package.json");
|
|
1685
|
-
if (
|
|
1686
|
-
const pkg = JSON.parse(
|
|
1903
|
+
if (existsSync3(candidate)) {
|
|
1904
|
+
const pkg = JSON.parse(readFileSync3(candidate, "utf8"));
|
|
1687
1905
|
if (pkg.name === "@tertium/gith")
|
|
1688
1906
|
return candidate;
|
|
1689
1907
|
}
|
|
@@ -1696,7 +1914,7 @@ function packageJsonPath() {
|
|
|
1696
1914
|
|
|
1697
1915
|
// src/components/version/version.ts
|
|
1698
1916
|
function readVersion() {
|
|
1699
|
-
const pkg = JSON.parse(
|
|
1917
|
+
const pkg = JSON.parse(readFileSync4(packageJsonPath(), "utf8"));
|
|
1700
1918
|
return pkg.version ?? "unknown";
|
|
1701
1919
|
}
|
|
1702
1920
|
function versionText() {
|
|
@@ -1714,9 +1932,14 @@ Start and finish work
|
|
|
1714
1932
|
gith create -r <version> release/<version> from develop
|
|
1715
1933
|
gith create -h <version> hotfix/<version> from main
|
|
1716
1934
|
gith create --release | --hotfix version from the latest tag: minor for release, patch for hotfix
|
|
1935
|
+
gith switch list local and origin branches, and switch to the one picked
|
|
1717
1936
|
gith switch -b <branch> switch to a branch, creating it from develop if missing (on a detached HEAD: from that commit)
|
|
1718
1937
|
gith switch -commit <sha> detach HEAD at a commit, to look at it (tracked files must be clean)
|
|
1938
|
+
gith switch --release | --hotfix switch to the newest release/* or hotfix/* (local or origin); -r <v> / -h <v> for one version
|
|
1719
1939
|
gith update merge origin/develop into this work branch
|
|
1940
|
+
gith finish --current-release | --current-hotfix release the newest release/* or hotfix/*: version, changelog, merge to main, tag, back to develop
|
|
1941
|
+
gith merge --develop merge this branch into develop
|
|
1942
|
+
gith merge pick the branch to merge, then the target (develop, release/*, hotfix/*)
|
|
1720
1943
|
gith merge [-r <v> | -h <v>] merge this branch into develop, release/<v> or hotfix/<v>
|
|
1721
1944
|
gith merge --current-release merge this branch into the newest release (local or origin)
|
|
1722
1945
|
gith merge --current-hotfix merge this branch into the newest hotfix (local or origin)
|
|
@@ -1737,7 +1960,8 @@ Clean up branches (every removal shows what it will remove first)
|
|
|
1737
1960
|
gith remove -b <branch> delete the branch here and on origin (either side may be missing)
|
|
1738
1961
|
gith remove -b <branch> --local delete only the local branch
|
|
1739
1962
|
gith remove -b <branch> --remote delete only the origin branch
|
|
1740
|
-
gith remove
|
|
1963
|
+
gith remove pick branches to delete from a list (here and on origin)
|
|
1964
|
+
gith remove --branch [-l <n>] same list, for branch pairs (here and on origin)
|
|
1741
1965
|
gith remove --branch --local pick from local branches
|
|
1742
1966
|
gith remove --branch --remote pick from origin branches
|
|
1743
1967
|
gith remove --obsolete [--local] [-days <n>] remove branches untouched for 90 days, after Y/n
|
|
@@ -1794,11 +2018,14 @@ try {
|
|
|
1794
2018
|
case "create":
|
|
1795
2019
|
create(parseFlags(rest));
|
|
1796
2020
|
break;
|
|
2021
|
+
case "finish":
|
|
2022
|
+
await finish(parseFlags(rest));
|
|
2023
|
+
break;
|
|
1797
2024
|
case "merge":
|
|
1798
|
-
merge(parseFlags(rest));
|
|
2025
|
+
await merge(parseFlags(rest));
|
|
1799
2026
|
break;
|
|
1800
2027
|
case "switch":
|
|
1801
|
-
switchBranch(parseFlags(rest));
|
|
2028
|
+
await switchBranch(parseFlags(rest));
|
|
1802
2029
|
break;
|
|
1803
2030
|
case "remove":
|
|
1804
2031
|
case "rm":
|