@tertium/gith 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -8
- package/bin/gith.js +375 -112
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
d b
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Git helper: the branch flow from [git-convention](https://github.com/Tertiumnon/git-convention) (
|
|
14
|
+
Git helper: the branch flow from [git-convention](https://github.com/Tertiumnon/git-convention) (work branches named develop--<goal>, `release/*` and `hotfix/*` containers, merges that sync both sides with origin), plus everyday git commands: status, switch, update, undo, save, squash, stash, remove, list, tree, sync, clean, reset, open, doctor and stat. Any command takes `--dry-run` to show the git commands it would run. No dependencies beyond git and [Bun](https://bun.com).
|
|
15
15
|
|
|
16
16
|
> **Very early state (0.1.0).** Commands and flags may change without notice, and the tool has only been run against throwaway test repositories. Check the result with `git log` and `git status` before you push.
|
|
17
17
|
|
|
@@ -26,20 +26,29 @@ gith --version
|
|
|
26
26
|
|
|
27
27
|
From a clone of this repository, build the CLI and link it: `bun install`, `bun run build`, `npm link`.
|
|
28
28
|
|
|
29
|
+
`bun install` also points git at `.githooks/`, so each commit runs `bun run typecheck`, `bun run format:check` and `bun run lint` first, and a failing check stops it. `bun run format` rewrites files to the formatter's style.
|
|
30
|
+
|
|
29
31
|
## Usage
|
|
30
32
|
|
|
31
33
|
```bash
|
|
32
34
|
# Start and finish work
|
|
33
|
-
gith create -b login-redirect # work branch from develop
|
|
35
|
+
gith create -b login-redirect # work branch develop--login-redirect from develop; stays on the current branch
|
|
34
36
|
gith create -r 1.2.0 # release/1.2.0 from develop
|
|
35
37
|
gith create -h 1.2.1 # hotfix/1.2.1 from main
|
|
36
38
|
gith create --release # release, version = minor after the latest tag
|
|
37
39
|
gith create --hotfix # hotfix, version = patch after the latest tag
|
|
38
|
-
gith switch
|
|
40
|
+
gith switch # list local and origin branches, and switch to the one you pick
|
|
41
|
+
gith switch -b login-redirect # switch to develop--login-redirect, creating it from develop if it is missing
|
|
39
42
|
gith switch -commit 3f2a1b9 # detach HEAD at that commit, to look at old code
|
|
43
|
+
gith switch --release # switch to the newest release/* (local or origin); -r 1.9.0 for one version
|
|
44
|
+
gith switch --hotfix # switch to the newest hotfix/* (local or origin); -h 1.2.1 for one version
|
|
40
45
|
gith update # merge origin/develop into this work branch
|
|
41
46
|
gith merge # this branch into develop
|
|
42
|
-
gith
|
|
47
|
+
gith finish --current-release # release the newest release/*: version, changelog, merge to main, tag, merge main to develop, then ask before deleting the branch
|
|
48
|
+
gith finish --current-hotfix # the same for the newest hotfix/*
|
|
49
|
+
gith merge # pick the branch to merge, then the target from the list
|
|
50
|
+
gith merge --main # this branch into main
|
|
51
|
+
gith merge -r 1.2.0 # this branch into release/1.2.0 (-r alone: the only release, or pick from the list)
|
|
43
52
|
gith merge -h 1.2.1 # this branch into hotfix/1.2.1
|
|
44
53
|
gith merge --current-release # this branch into the newest release/* (local or origin)
|
|
45
54
|
gith merge --current-hotfix # this branch into the newest hotfix/* (local or origin)
|
|
@@ -49,7 +58,7 @@ gith release --major # npm version major, from develop into main
|
|
|
49
58
|
|
|
50
59
|
# Fix history
|
|
51
60
|
gith undo # undo the last commit, keep its changes staged (not if pushed)
|
|
52
|
-
gith save [-m "msg"]
|
|
61
|
+
gith save [-m "msg"] [--all] # --all stages every change first; fold staged changes into the last commit, or make a new commit with them if the branch has none yet (asks for a message); pushes if origin differs
|
|
53
62
|
gith squash -limit 3 [-m "msg"] # combine the last 3 commits into one (keeps the oldest message)
|
|
54
63
|
|
|
55
64
|
# Save and reset work
|
|
@@ -59,6 +68,7 @@ gith reset # discard uncommitted changes, then clean
|
|
|
59
68
|
gith clean # remove untracked files and directories
|
|
60
69
|
|
|
61
70
|
# Clean up branches (every removal shows what it will remove first)
|
|
71
|
+
gith remove # pick branches to delete from the list (here and on origin)
|
|
62
72
|
gith remove -b login-redirect # delete the branch here and on origin (rm is an alias)
|
|
63
73
|
gith remove -b login-redirect --local # delete only the local branch
|
|
64
74
|
gith remove -b login-redirect --remote # delete only the origin branch
|
|
@@ -113,9 +123,12 @@ One rule applies to every command: **a single dash always takes a value, and a d
|
|
|
113
123
|
| `-b <name>`, `-branch <name>` | value | `create`, `switch`, `remove` |
|
|
114
124
|
| `--branch` | alone: open the picker | `remove` |
|
|
115
125
|
| `-r <version>`, `-release <version>` | value | `create`, `merge` |
|
|
126
|
+
| `-r` alone | the only release, or a pick from the list | `merge` |
|
|
116
127
|
| `--release` | alone: version from the latest tag | `create` |
|
|
128
|
+
| `--main` | alone: merge into main | `merge` |
|
|
117
129
|
| `--current-release`, `--current-hotfix` | switch: merge into the newest release or hotfix | `merge` |
|
|
118
130
|
| `-h <version>`, `-hotfix <version>` | value | `create`, `merge` |
|
|
131
|
+
| `-h` alone | the only hotfix, or a pick from the list | `merge` |
|
|
119
132
|
| `--hotfix` | alone: version from the latest tag | `create` |
|
|
120
133
|
| `-limit <n>` | value | `squash`: how many commits to combine |
|
|
121
134
|
| `-m <msg>`, `-message <msg>` | value | `squash` |
|
|
@@ -138,10 +151,10 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
138
151
|
|
|
139
152
|
## Git commands
|
|
140
153
|
|
|
141
|
-
- **`switch -b`** switches to a branch. If
|
|
154
|
+
- **`switch -b`** switches to a branch. If a branch with that name exists (locally or on origin), it is checked out. Otherwise, if `develop--<name>` exists, that one is checked out. If neither exists, `develop--<name>` is created from `origin/develop`, the same way as `create -b`, and checked out.
|
|
142
155
|
- **`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
156
|
- **`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
|
|
157
|
+
- **`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
158
|
- **`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
159
|
- **`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
160
|
- **`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.
|
|
@@ -172,10 +185,12 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
172
185
|
|
|
173
186
|
## What it does
|
|
174
187
|
|
|
175
|
-
- **`create -b`** creates
|
|
188
|
+
- **`create -b`** creates `develop--<name>` from `origin/develop`, pushes it with upstream tracking, and leaves the current branch checked out. The prefix shows the source branch; `create -b develop` and `create -b main` are refused. It does not touch the working tree, so it works with uncommitted changes.
|
|
176
189
|
- **`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
190
|
- **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
191
|
- **`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.
|
|
192
|
+
- **`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.
|
|
193
|
+
- **`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
194
|
- **`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
195
|
|
|
181
196
|
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
|
@@ -6,8 +6,8 @@ import { exit } from "node:process";
|
|
|
6
6
|
// src/components/args/args.constants.ts
|
|
7
7
|
var SPECS = new Map([
|
|
8
8
|
["branch", "auto"],
|
|
9
|
-
["release", "
|
|
10
|
-
["hotfix", "
|
|
9
|
+
["release", "optional"],
|
|
10
|
+
["hotfix", "optional"],
|
|
11
11
|
["limit", "value"],
|
|
12
12
|
["message", "value"],
|
|
13
13
|
["name", "value"],
|
|
@@ -28,6 +28,8 @@ var SPECS = new Map([
|
|
|
28
28
|
["stashes", "switch"],
|
|
29
29
|
["current-release", "switch"],
|
|
30
30
|
["current-hotfix", "switch"],
|
|
31
|
+
["develop", "switch"],
|
|
32
|
+
["main", "switch"],
|
|
31
33
|
["pockets", "switch"],
|
|
32
34
|
["detached", "switch"],
|
|
33
35
|
["all", "switch"],
|
|
@@ -88,10 +90,14 @@ function parseFlags(argv) {
|
|
|
88
90
|
if (style === "single") {
|
|
89
91
|
if (kind === "switch")
|
|
90
92
|
throw new Error(`${token} is a switch; write --${name}.`);
|
|
91
|
-
if (
|
|
93
|
+
if (hasNext) {
|
|
94
|
+
flags.set(name, next);
|
|
95
|
+
i++;
|
|
96
|
+
} else if (kind === "optional") {
|
|
97
|
+
flags.set(name, "");
|
|
98
|
+
} else {
|
|
92
99
|
throw new Error(`${token} needs a value.`);
|
|
93
|
-
|
|
94
|
-
i++;
|
|
100
|
+
}
|
|
95
101
|
} else if (kind === "switch") {
|
|
96
102
|
flags.set(name, "true");
|
|
97
103
|
} else if (kind === "value") {
|
|
@@ -427,9 +433,127 @@ function find(flags) {
|
|
|
427
433
|
`));
|
|
428
434
|
}
|
|
429
435
|
|
|
436
|
+
// src/components/flow/flow.ts
|
|
437
|
+
import { execSync } from "node:child_process";
|
|
438
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
439
|
+
|
|
440
|
+
// src/components/prompt/prompt.ts
|
|
441
|
+
import { emitKeypressEvents } from "node:readline";
|
|
442
|
+
import { createInterface } from "node:readline/promises";
|
|
443
|
+
|
|
444
|
+
// src/components/prompt/prompt.utils.ts
|
|
445
|
+
function moveIndex(index, delta, length) {
|
|
446
|
+
if (length <= 0)
|
|
447
|
+
return 0;
|
|
448
|
+
return ((index + delta) % length + length) % length;
|
|
449
|
+
}
|
|
450
|
+
function toggled(checked, index) {
|
|
451
|
+
const next = new Set(checked);
|
|
452
|
+
if (next.has(index))
|
|
453
|
+
next.delete(index);
|
|
454
|
+
else
|
|
455
|
+
next.add(index);
|
|
456
|
+
return next;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/components/prompt/prompt.ts
|
|
460
|
+
var assumeYes = false;
|
|
461
|
+
function setAssumeYes(on) {
|
|
462
|
+
assumeYes = on;
|
|
463
|
+
}
|
|
464
|
+
async function confirm(question) {
|
|
465
|
+
if (isDryRun()) {
|
|
466
|
+
console.log(`dry run: would ask: ${question}`);
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
if (assumeYes)
|
|
470
|
+
return true;
|
|
471
|
+
if (!process.stdin.isTTY)
|
|
472
|
+
throw new Error("this needs a terminal to confirm; pass --yes to skip the question.");
|
|
473
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
474
|
+
try {
|
|
475
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
476
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
477
|
+
} finally {
|
|
478
|
+
rl.close();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async function ask(question) {
|
|
482
|
+
if (isDryRun()) {
|
|
483
|
+
console.log(`dry run: would ask: ${question}`);
|
|
484
|
+
return "<message>";
|
|
485
|
+
}
|
|
486
|
+
if (!process.stdin.isTTY)
|
|
487
|
+
throw new Error("needs a terminal to ask; pass -m <message>.");
|
|
488
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
489
|
+
try {
|
|
490
|
+
return (await rl.question(question)).trim();
|
|
491
|
+
} finally {
|
|
492
|
+
rl.close();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async function select(question, options, multi) {
|
|
496
|
+
if (!process.stdin.isTTY)
|
|
497
|
+
throw new Error("this needs a terminal to pick.");
|
|
498
|
+
return new Promise((resolve) => {
|
|
499
|
+
const stdin = process.stdin;
|
|
500
|
+
let index = 0;
|
|
501
|
+
let checked = new Set;
|
|
502
|
+
let drawnLines = 0;
|
|
503
|
+
const draw = () => {
|
|
504
|
+
if (drawnLines > 0)
|
|
505
|
+
process.stdout.write(`\x1B[${drawnLines}A\x1B[0J`);
|
|
506
|
+
const hint = multi ? "space checks, enter confirms, esc cancels" : "enter selects, esc cancels";
|
|
507
|
+
const lines = [`${question} (${hint})`];
|
|
508
|
+
options.forEach((option, i) => {
|
|
509
|
+
const cursor = i === index ? ">" : " ";
|
|
510
|
+
const mark = multi ? checked.has(i) ? "[x]" : "[ ]" : "";
|
|
511
|
+
lines.push(`${cursor} ${mark} ${option.label}`.replace(/ +$/, ""));
|
|
512
|
+
});
|
|
513
|
+
process.stdout.write(`${lines.join(`
|
|
514
|
+
`)}
|
|
515
|
+
`);
|
|
516
|
+
drawnLines = lines.length;
|
|
517
|
+
};
|
|
518
|
+
const finish = (picked) => {
|
|
519
|
+
stdin.off("keypress", onKey);
|
|
520
|
+
stdin.setRawMode(false);
|
|
521
|
+
stdin.pause();
|
|
522
|
+
process.stdout.write(`
|
|
523
|
+
`);
|
|
524
|
+
resolve(picked);
|
|
525
|
+
};
|
|
526
|
+
const onKey = (_text, key) => {
|
|
527
|
+
if (!key)
|
|
528
|
+
return;
|
|
529
|
+
if (key.ctrl && key.name === "c" || key.name === "escape") {
|
|
530
|
+
finish([]);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (key.name === "return") {
|
|
534
|
+
const picked = multi ? [...checked].sort((a, b) => a - b).map((i) => options[i]?.name ?? "") : [options[index]?.name ?? ""];
|
|
535
|
+
finish(picked.filter(Boolean));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (key.name === "up")
|
|
539
|
+
index = moveIndex(index, -1, options.length);
|
|
540
|
+
else if (key.name === "down")
|
|
541
|
+
index = moveIndex(index, 1, options.length);
|
|
542
|
+
else if (key.name === "space" && multi)
|
|
543
|
+
checked = toggled(checked, index);
|
|
544
|
+
draw();
|
|
545
|
+
};
|
|
546
|
+
emitKeypressEvents(stdin);
|
|
547
|
+
stdin.setRawMode(true);
|
|
548
|
+
stdin.resume();
|
|
549
|
+
stdin.on("keypress", onKey);
|
|
550
|
+
draw();
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
430
554
|
// src/components/rules/rules.constants.ts
|
|
431
555
|
var CONTAINER = /^(develop|main|release\/.+|hotfix\/.+)$/;
|
|
432
|
-
var TARGET = /^(develop|release\/.+|hotfix\/.+)$/;
|
|
556
|
+
var TARGET = /^(main|develop|release\/.+|hotfix\/.+)$/;
|
|
433
557
|
|
|
434
558
|
// src/components/rules/rules.ts
|
|
435
559
|
function isContainer(name) {
|
|
@@ -478,7 +602,7 @@ function nextVersion(tags, bump) {
|
|
|
478
602
|
}
|
|
479
603
|
|
|
480
604
|
// src/components/flow/flow.utils.ts
|
|
481
|
-
function
|
|
605
|
+
function containers(kind) {
|
|
482
606
|
const prefix = `${kind}/`;
|
|
483
607
|
const local = captureArgs([
|
|
484
608
|
"for-each-ref",
|
|
@@ -489,15 +613,19 @@ function newestContainer(kind) {
|
|
|
489
613
|
const origin = captureArgs(["ls-remote", "--heads", "origin"]).split(`
|
|
490
614
|
`).map((line) => (line.split("\t")[1] ?? "").replace(/^refs\/heads\//, ""));
|
|
491
615
|
const candidates = new Set([...local, ...origin].filter((name) => name.startsWith(prefix)));
|
|
492
|
-
|
|
616
|
+
const versioned = [];
|
|
493
617
|
for (const name of candidates) {
|
|
494
618
|
const version = parseSemver(name.slice(prefix.length));
|
|
495
|
-
if (version
|
|
496
|
-
|
|
619
|
+
if (version)
|
|
620
|
+
versioned.push({ name, version });
|
|
497
621
|
}
|
|
622
|
+
return versioned.sort((a, b) => compare(b.version, a.version)).map((item) => item.name);
|
|
623
|
+
}
|
|
624
|
+
function newestContainer(kind) {
|
|
625
|
+
const [newest] = containers(kind);
|
|
498
626
|
if (!newest)
|
|
499
627
|
throw new Error(`no ${kind} branch exists, locally or on origin.`);
|
|
500
|
-
return newest
|
|
628
|
+
return newest;
|
|
501
629
|
}
|
|
502
630
|
function nextContainerVersion(kind) {
|
|
503
631
|
const bump = kind === "release" ? "minor" : "patch";
|
|
@@ -534,44 +662,70 @@ function create(flags) {
|
|
|
534
662
|
}
|
|
535
663
|
open(kind, value || nextContainerVersion(kind));
|
|
536
664
|
}
|
|
537
|
-
function merge(flags) {
|
|
665
|
+
async function merge(flags) {
|
|
666
|
+
if (flags.size === 0)
|
|
667
|
+
return mergePick();
|
|
538
668
|
const kind = onlyOne(flags, [
|
|
669
|
+
"develop",
|
|
670
|
+
"main",
|
|
539
671
|
"release",
|
|
540
672
|
"hotfix",
|
|
541
673
|
"current-release",
|
|
542
674
|
"current-hotfix"
|
|
543
675
|
]);
|
|
544
676
|
let target = "develop";
|
|
545
|
-
if (kind === "
|
|
677
|
+
if (kind === "develop")
|
|
678
|
+
target = "develop";
|
|
679
|
+
else if (kind === "main")
|
|
680
|
+
target = "main";
|
|
681
|
+
else if (kind === "current-release")
|
|
546
682
|
target = newestContainer("release");
|
|
547
683
|
else if (kind === "current-hotfix")
|
|
548
684
|
target = newestContainer("hotfix");
|
|
549
685
|
else if (kind) {
|
|
550
686
|
const version = flags.get(kind) ?? "";
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
target =
|
|
687
|
+
const picked = version === "" ? await pickContainer(kind) : containerName(kind, version);
|
|
688
|
+
if (picked === undefined)
|
|
689
|
+
return;
|
|
690
|
+
target = picked;
|
|
555
691
|
}
|
|
556
|
-
mergeInto(target);
|
|
692
|
+
await mergeInto(target);
|
|
693
|
+
}
|
|
694
|
+
async function pickContainer(kind) {
|
|
695
|
+
const names = containers(kind);
|
|
696
|
+
if (names.length === 0)
|
|
697
|
+
throw new Error(`no ${kind} branch exists, locally or on origin.`);
|
|
698
|
+
if (names.length === 1)
|
|
699
|
+
return names[0];
|
|
700
|
+
if (!process.stdin.isTTY) {
|
|
701
|
+
throw new Error(`several ${kind} branches exist; pass -${kind === "release" ? "r" : "h"} <version> instead; the list needs a terminal`);
|
|
702
|
+
}
|
|
703
|
+
const [picked] = await select(`Merge into which ${kind}?`, names.map((name) => ({ name, label: name })), false);
|
|
704
|
+
return picked;
|
|
705
|
+
}
|
|
706
|
+
function workBranchName(goal, from) {
|
|
707
|
+
return from === undefined ? `develop--${goal}` : goal;
|
|
557
708
|
}
|
|
558
709
|
function createBranch(goal, from) {
|
|
559
710
|
if (isContainer(goal))
|
|
560
711
|
throw new Error(`'${goal}' is a container name; pick a goal.`);
|
|
561
|
-
|
|
562
|
-
|
|
712
|
+
const name = workBranchName(goal, from);
|
|
713
|
+
if (hasOnOrigin(name))
|
|
714
|
+
throw new Error(`'${name}' already exists on origin.`);
|
|
563
715
|
if (from === undefined) {
|
|
564
716
|
gitArgs(["fetch", "origin", "develop"]);
|
|
565
|
-
gitArgs(["branch",
|
|
717
|
+
gitArgs(["branch", name, "origin/develop"]);
|
|
566
718
|
} else {
|
|
567
|
-
gitArgs(["branch",
|
|
719
|
+
gitArgs(["branch", name, from]);
|
|
568
720
|
}
|
|
569
|
-
gitArgs(["push", "-u", "origin",
|
|
721
|
+
gitArgs(["push", "-u", "origin", name]);
|
|
722
|
+
return name;
|
|
570
723
|
}
|
|
571
|
-
function mergeInto(target) {
|
|
724
|
+
async function mergeInto(target, from) {
|
|
572
725
|
if (!isTarget(target))
|
|
573
726
|
throw new Error(`'${target}' is not a valid target.`);
|
|
574
|
-
const
|
|
727
|
+
const current = capture("rev-parse --abbrev-ref HEAD");
|
|
728
|
+
const source = from ?? current;
|
|
575
729
|
if (isContainer(source))
|
|
576
730
|
throw new Error(`'${source}' is a container and cannot be merged from.`);
|
|
577
731
|
if (source === target)
|
|
@@ -579,6 +733,8 @@ function mergeInto(target) {
|
|
|
579
733
|
if (!hasOnOrigin(target))
|
|
580
734
|
throw new Error(`'${target}' does not exist on origin.`);
|
|
581
735
|
assertCleanTree();
|
|
736
|
+
if (source !== current)
|
|
737
|
+
gitArgs(["switch", "--", source]);
|
|
582
738
|
if (hasOnOrigin(source))
|
|
583
739
|
git(`pull origin ${source}`);
|
|
584
740
|
git(`push -u origin ${source}`);
|
|
@@ -598,44 +754,116 @@ function mergeInto(target) {
|
|
|
598
754
|
assertSynced([target, source]);
|
|
599
755
|
console.log(`
|
|
600
756
|
✅ '${source}' merged into '${target}', both in sync with origin.`);
|
|
757
|
+
await deleteMergedBranch(source);
|
|
601
758
|
}
|
|
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 });
|
|
759
|
+
async function deleteMergedBranch(source) {
|
|
760
|
+
const local = hasLocal(source);
|
|
761
|
+
const origin = hasOnOrigin(source);
|
|
762
|
+
let yes;
|
|
619
763
|
try {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
764
|
+
yes = await confirm(`Delete '${source}' here and on origin, it is merged? [Y/n] `);
|
|
765
|
+
} catch {
|
|
766
|
+
yes = false;
|
|
767
|
+
}
|
|
768
|
+
if (!yes) {
|
|
769
|
+
console.log(`'${source}' kept.`);
|
|
770
|
+
return;
|
|
624
771
|
}
|
|
772
|
+
if (local)
|
|
773
|
+
gitArgs(["branch", "-d", "--", source]);
|
|
774
|
+
if (origin)
|
|
775
|
+
gitArgs(["push", "origin", "--delete", source]);
|
|
625
776
|
}
|
|
626
|
-
async function
|
|
777
|
+
async function mergePick() {
|
|
778
|
+
if (!process.stdin.isTTY) {
|
|
779
|
+
throw new Error("pass -r <version> or -h <version> instead; the pickers need a terminal");
|
|
780
|
+
}
|
|
781
|
+
const current = capture("rev-parse --abbrev-ref HEAD");
|
|
782
|
+
const local = captureArgs([
|
|
783
|
+
"for-each-ref",
|
|
784
|
+
"--format=%(refname:short)",
|
|
785
|
+
"refs/heads"
|
|
786
|
+
]).split(`
|
|
787
|
+
`).filter(Boolean);
|
|
788
|
+
const sources = local.filter((name) => !isContainer(name)).sort((a, b) => a.localeCompare(b)).map((name) => ({
|
|
789
|
+
name,
|
|
790
|
+
label: name === current ? `${name} (current)` : name
|
|
791
|
+
}));
|
|
792
|
+
if (sources.length === 0)
|
|
793
|
+
throw new Error("no work branch to merge.");
|
|
794
|
+
const [source] = await select("Which branch to merge?", sources, false);
|
|
795
|
+
if (source === undefined)
|
|
796
|
+
return;
|
|
797
|
+
const origin = captureArgs(["ls-remote", "--heads", "origin"]).split(`
|
|
798
|
+
`).map((line) => (line.split("\t")[1] ?? "").replace(/^refs\/heads\//, "")).filter(Boolean);
|
|
799
|
+
const targets = [...new Set([...local, ...origin])].filter((name) => isTarget(name) && name !== source).sort((a, b) => a.localeCompare(b)).map((name) => ({
|
|
800
|
+
name,
|
|
801
|
+
label: local.includes(name) ? name : `${name} (origin)`
|
|
802
|
+
}));
|
|
803
|
+
if (targets.length === 0)
|
|
804
|
+
throw new Error("no develop, release or hotfix branch to merge into.");
|
|
805
|
+
const [target] = await select(`Merge '${source}' into which?`, targets, false);
|
|
806
|
+
if (target === undefined)
|
|
807
|
+
return;
|
|
808
|
+
await mergeInto(target, source);
|
|
809
|
+
}
|
|
810
|
+
function alignVersion(version) {
|
|
811
|
+
if (!existsSync2("package.json"))
|
|
812
|
+
return;
|
|
813
|
+
const current = parsePackageVersion(readFileSync2("package.json", "utf8"));
|
|
814
|
+
if (current === version)
|
|
815
|
+
return;
|
|
627
816
|
if (isDryRun()) {
|
|
628
|
-
console.log(
|
|
629
|
-
|
|
817
|
+
console.log(`> npm version ${version} --no-git-tag-version --ignore-scripts`);
|
|
818
|
+
} else {
|
|
819
|
+
execSync(`npm version ${version} --no-git-tag-version --ignore-scripts`, {
|
|
820
|
+
stdio: "inherit"
|
|
821
|
+
});
|
|
630
822
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
823
|
+
gitArgs(["add", "--", "package.json"]);
|
|
824
|
+
gitArgs(["commit", "-m", version, "--", "package.json"]);
|
|
825
|
+
}
|
|
826
|
+
function addChangelogEntry(version) {
|
|
827
|
+
if (!existsSync2("package.json"))
|
|
828
|
+
return;
|
|
829
|
+
const file = "CHANGELOG.md";
|
|
830
|
+
if (existsSync2(file) && readFileSync2(file, "utf8").includes(`## ${version} - `))
|
|
831
|
+
return;
|
|
832
|
+
changelog(new Map);
|
|
833
|
+
}
|
|
834
|
+
async function finish(flags) {
|
|
835
|
+
const kind = onlyOne(flags, ["current-release", "current-hotfix"]);
|
|
836
|
+
if (!kind || flags.size > 1)
|
|
837
|
+
throw new Error("usage: gith finish --current-release | --current-hotfix");
|
|
838
|
+
const containerKind = kind === "current-release" ? "release" : "hotfix";
|
|
839
|
+
const branch = newestContainer(containerKind);
|
|
840
|
+
const version = branch.slice(containerKind.length + 1);
|
|
841
|
+
if (parseSemver(version) === undefined)
|
|
842
|
+
throw new Error(`'${branch}' is not named with a version (X.Y.Z).`);
|
|
843
|
+
assertCleanTree();
|
|
844
|
+
gitArgs(["switch", "--", branch]);
|
|
845
|
+
gitArgs(["pull", "--ff-only", "origin", branch]);
|
|
846
|
+
alignVersion(version);
|
|
847
|
+
addChangelogEntry(version);
|
|
848
|
+
gitArgs(["switch", "--", "main"]);
|
|
849
|
+
gitArgs(["pull", "--ff-only", "origin", "main"]);
|
|
634
850
|
try {
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
|
|
851
|
+
gitArgs(["merge", "--no-ff", branch, "-m", `Merge '${branch}' into main`]);
|
|
852
|
+
} catch {
|
|
853
|
+
gitArgs(["merge", "--abort"]);
|
|
854
|
+
throw new Error(`merging '${branch}' into main conflicts; resolve it by hand, then retry.`);
|
|
855
|
+
}
|
|
856
|
+
gitArgs(["tag", "-a", `v${version}`, "-m", `v${version}`]);
|
|
857
|
+
gitArgs(["push", "origin", "main"]);
|
|
858
|
+
gitArgs(["push", "origin", `v${version}`]);
|
|
859
|
+
if (hasOnOrigin("develop")) {
|
|
860
|
+
gitArgs(["switch", "--", "develop"]);
|
|
861
|
+
gitArgs(["pull", "--ff-only", "origin", "develop"]);
|
|
862
|
+
gitArgs(["merge", "--no-edit", "main"]);
|
|
863
|
+
gitArgs(["push", "origin", "develop"]);
|
|
638
864
|
}
|
|
865
|
+
gitArgs(["switch", "--", "main"]);
|
|
866
|
+
await deleteMergedBranch(branch);
|
|
639
867
|
}
|
|
640
868
|
|
|
641
869
|
// src/components/history/history.utils.ts
|
|
@@ -690,10 +918,12 @@ function undo(flags) {
|
|
|
690
918
|
}
|
|
691
919
|
async function save(flags) {
|
|
692
920
|
const message = flags.get("message");
|
|
693
|
-
const extra = [...flags.keys()].filter((key) => key !== "message");
|
|
921
|
+
const extra = [...flags.keys()].filter((key) => key !== "message" && key !== "all");
|
|
694
922
|
if (extra.length > 0)
|
|
695
|
-
throw new Error("usage: gith save [-m <message>]");
|
|
923
|
+
throw new Error("usage: gith save [-m <message>] [--all]");
|
|
696
924
|
const branch = currentBranch();
|
|
925
|
+
if (flags.has("all"))
|
|
926
|
+
gitArgs(["add", "-A"]);
|
|
697
927
|
if (captureArgs(["diff", "--cached", "--name-only"]) === "") {
|
|
698
928
|
throw new Error("nothing staged; stage the changes to add to the last commit.");
|
|
699
929
|
}
|
|
@@ -862,7 +1092,7 @@ function stashes(limit, withGorlum) {
|
|
|
862
1092
|
]).split(`
|
|
863
1093
|
`).filter(Boolean);
|
|
864
1094
|
if (rows.length === 0) {
|
|
865
|
-
console.log("No stashes.");
|
|
1095
|
+
console.log(withGorlum ? "There is nothing in my pockets!" : "No stashes.");
|
|
866
1096
|
return;
|
|
867
1097
|
}
|
|
868
1098
|
const shown = rows.slice(0, limit);
|
|
@@ -893,9 +1123,6 @@ function originTags() {
|
|
|
893
1123
|
`).filter(Boolean).map((line) => (line.split("\t")[1] ?? "").replace(/^refs\/tags\//, "")).filter((ref) => ref && !ref.endsWith("^{}"));
|
|
894
1124
|
}
|
|
895
1125
|
|
|
896
|
-
// src/components/maintenance/maintenance.ts
|
|
897
|
-
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
898
|
-
|
|
899
1126
|
// src/components/maintenance/maintenance.constants.ts
|
|
900
1127
|
var DEFAULT_DAYS = 90;
|
|
901
1128
|
|
|
@@ -944,7 +1171,21 @@ function parsePositive(flags, name) {
|
|
|
944
1171
|
}
|
|
945
1172
|
|
|
946
1173
|
// src/components/maintenance/maintenance.ts
|
|
947
|
-
function switchBranch(flags) {
|
|
1174
|
+
async function switchBranch(flags) {
|
|
1175
|
+
if (flags.size === 0)
|
|
1176
|
+
return switchPick();
|
|
1177
|
+
const kind = onlyOne(flags, ["release", "hotfix"]);
|
|
1178
|
+
if (kind !== undefined) {
|
|
1179
|
+
if (flags.size > 1)
|
|
1180
|
+
throw new Error(`usage: gith switch --${kind} | -${kind === "release" ? "r" : "h"} <version>`);
|
|
1181
|
+
const version = flags.get(kind) ?? "";
|
|
1182
|
+
const name = version === "" ? newestContainer(kind) : containerName(kind, version);
|
|
1183
|
+
if (!hasLocal(name) && !hasOnOrigin(name)) {
|
|
1184
|
+
throw new Error(`'${name}' does not exist, locally or on origin.`);
|
|
1185
|
+
}
|
|
1186
|
+
gitArgs(["switch", "--", name]);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
948
1189
|
const commit = flags.get("commit");
|
|
949
1190
|
if (commit !== undefined) {
|
|
950
1191
|
if (flags.has("branch") || flags.size > 1)
|
|
@@ -953,10 +1194,14 @@ function switchBranch(flags) {
|
|
|
953
1194
|
return;
|
|
954
1195
|
}
|
|
955
1196
|
const branch = requireBranch(flags, "switch");
|
|
956
|
-
|
|
957
|
-
|
|
1197
|
+
const from = isDetached() ? "HEAD" : undefined;
|
|
1198
|
+
const exists = (name) => hasLocal(name) || hasOnOrigin(name);
|
|
1199
|
+
let name = branch;
|
|
1200
|
+
if (!exists(branch)) {
|
|
1201
|
+
const made = workBranchName(branch, from);
|
|
1202
|
+
name = exists(made) ? made : createBranch(branch, from);
|
|
958
1203
|
}
|
|
959
|
-
gitArgs(["switch", "--",
|
|
1204
|
+
gitArgs(["switch", "--", name]);
|
|
960
1205
|
}
|
|
961
1206
|
function candidates(scope, local, remote, current) {
|
|
962
1207
|
const notContainer = (name) => !isContainer(name);
|
|
@@ -969,20 +1214,6 @@ function candidates(scope, local, remote, current) {
|
|
|
969
1214
|
}
|
|
970
1215
|
return local.filter((name) => notContainer(name) && notCurrent(name) && remote.includes(name)).map((name) => ({ name, label: `${name} (local + origin)` }));
|
|
971
1216
|
}
|
|
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
1217
|
function branchTimes(output) {
|
|
987
1218
|
const times = new Map;
|
|
988
1219
|
for (const line of output.split(`
|
|
@@ -1028,7 +1259,7 @@ function detachedOptions(times, current) {
|
|
|
1028
1259
|
return options.sort((x, y) => x.name.localeCompare(y.name));
|
|
1029
1260
|
}
|
|
1030
1261
|
async function remove(flags) {
|
|
1031
|
-
const name = flags.get("branch");
|
|
1262
|
+
const name = flags.get("branch") ?? "";
|
|
1032
1263
|
const side = onlyOne(flags, ["local", "remote"]);
|
|
1033
1264
|
const scope = side ?? "pair";
|
|
1034
1265
|
const obsoleteOn = flags.has("obsolete");
|
|
@@ -1041,14 +1272,12 @@ async function remove(flags) {
|
|
|
1041
1272
|
throw new Error("--all only applies to --detached.");
|
|
1042
1273
|
if (obsoleteOn || detachedOn) {
|
|
1043
1274
|
const usage = detachedOn ? "usage: gith remove --detached [--all | -l <n>]" : "usage: gith remove --obsolete [--local] [-days <n>] [-l <n>]";
|
|
1044
|
-
if (
|
|
1275
|
+
if (flags.has("branch") || obsoleteOn && side === "remote" || detachedOn && side)
|
|
1045
1276
|
throw new Error(usage);
|
|
1046
1277
|
if (detachedOn)
|
|
1047
1278
|
return detached(flags);
|
|
1048
1279
|
return obsolete(flags, side === "local" ? "local" : "pair");
|
|
1049
1280
|
}
|
|
1050
|
-
if (name === undefined)
|
|
1051
|
-
throw new Error("usage: gith remove -b <branch> [--local | --remote], or gith remove --branch to pick");
|
|
1052
1281
|
if (name) {
|
|
1053
1282
|
if (flags.has("limit"))
|
|
1054
1283
|
throw new Error("-limit only applies to listings: gith remove --branch.");
|
|
@@ -1103,25 +1332,49 @@ async function clean(flags) {
|
|
|
1103
1332
|
}
|
|
1104
1333
|
gitArgs(["clean", "-fd"]);
|
|
1105
1334
|
}
|
|
1106
|
-
|
|
1335
|
+
var REMOVE_ACTION = {
|
|
1336
|
+
empty: "No branches to remove.",
|
|
1337
|
+
prompt: "Remove which?",
|
|
1338
|
+
multi: true
|
|
1339
|
+
};
|
|
1340
|
+
async function choose(options, limit, action = REMOVE_ACTION) {
|
|
1107
1341
|
if (options.length === 0) {
|
|
1108
|
-
console.log(
|
|
1342
|
+
console.log(action.empty);
|
|
1109
1343
|
return [];
|
|
1110
1344
|
}
|
|
1111
1345
|
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
1346
|
if (shown.length < options.length) {
|
|
1116
1347
|
console.log(`showing ${shown.length} of ${options.length}; -limit <n> shows more`);
|
|
1117
1348
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1349
|
+
return select(action.prompt, shown, action.multi);
|
|
1350
|
+
}
|
|
1351
|
+
function switchOptions() {
|
|
1352
|
+
const local = localBranches();
|
|
1353
|
+
const origin = remoteBranches();
|
|
1354
|
+
const current = currentBranch2();
|
|
1355
|
+
const names = [...new Set([...local, ...origin])].sort((a, b) => a.localeCompare(b));
|
|
1356
|
+
return names.map((name) => {
|
|
1357
|
+
const where = local.includes(name) && origin.includes(name) ? "local + origin" : local.includes(name) ? "local" : "origin";
|
|
1358
|
+
return {
|
|
1359
|
+
name,
|
|
1360
|
+
label: `${name} (${where}${name === current ? ", current" : ""})`
|
|
1361
|
+
};
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
async function switchPick() {
|
|
1365
|
+
if (!process.stdin.isTTY) {
|
|
1366
|
+
throw new Error("pass a branch name with -b, e.g. gith switch -b <branch>; the list needs a terminal");
|
|
1124
1367
|
}
|
|
1368
|
+
const picked = await choose(switchOptions(), undefined, {
|
|
1369
|
+
empty: "No branches.",
|
|
1370
|
+
prompt: "Switch to which?",
|
|
1371
|
+
multi: false
|
|
1372
|
+
});
|
|
1373
|
+
if (picked.length === 0)
|
|
1374
|
+
return;
|
|
1375
|
+
if (picked.length > 1)
|
|
1376
|
+
throw new Error("pick exactly one branch to switch to.");
|
|
1377
|
+
gitArgs(["switch", "--", picked[0] ?? ""]);
|
|
1125
1378
|
}
|
|
1126
1379
|
function requireTerminal() {
|
|
1127
1380
|
if (!process.stdin.isTTY) {
|
|
@@ -1147,14 +1400,14 @@ async function removeBranches(scope, names, ask = true) {
|
|
|
1147
1400
|
});
|
|
1148
1401
|
if (plan.length > 0)
|
|
1149
1402
|
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
1403
|
for (const { name, local, origin } of plan) {
|
|
1155
1404
|
const sides = [local && "local", origin && "origin"].filter(Boolean).join(" + ");
|
|
1156
1405
|
console.log(` ${name} (${sides})`);
|
|
1157
1406
|
}
|
|
1407
|
+
if (ask && plan.length > 0 && !await confirm(plan.length === 1 ? "Remove this branch? [Y/n] " : `Remove these ${plan.length} branches? [Y/n] `)) {
|
|
1408
|
+
console.log("Nothing removed.");
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1158
1411
|
for (const { name, local, origin } of plan) {
|
|
1159
1412
|
if (local)
|
|
1160
1413
|
gitArgs(["branch", "-d", "--", name]);
|
|
@@ -1264,12 +1517,12 @@ function openerFor(platform, url) {
|
|
|
1264
1517
|
var KINDS = ["patch", "minor", "major"];
|
|
1265
1518
|
|
|
1266
1519
|
// src/components/release/release.utils.ts
|
|
1267
|
-
import { execSync } from "node:child_process";
|
|
1520
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
1268
1521
|
function npmVersion(kind) {
|
|
1269
1522
|
console.log(`> npm version ${kind}`);
|
|
1270
1523
|
if (isDryRun())
|
|
1271
1524
|
return;
|
|
1272
|
-
|
|
1525
|
+
execSync2(`npm version ${kind}`, { stdio: "inherit" });
|
|
1273
1526
|
}
|
|
1274
1527
|
|
|
1275
1528
|
// src/components/release/release.ts
|
|
@@ -1660,7 +1913,7 @@ function tree(flags) {
|
|
|
1660
1913
|
}
|
|
1661
1914
|
|
|
1662
1915
|
// src/components/version/version.ts
|
|
1663
|
-
import { readFileSync as
|
|
1916
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1664
1917
|
|
|
1665
1918
|
// src/components/version/version.constants.ts
|
|
1666
1919
|
var MASCOT = [
|
|
@@ -1675,15 +1928,15 @@ var MASCOT = [
|
|
|
1675
1928
|
];
|
|
1676
1929
|
|
|
1677
1930
|
// src/components/version/version.utils.ts
|
|
1678
|
-
import { existsSync as
|
|
1931
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1679
1932
|
import path2 from "node:path";
|
|
1680
1933
|
import { fileURLToPath } from "node:url";
|
|
1681
1934
|
function packageJsonPath() {
|
|
1682
1935
|
let dir = path2.dirname(fileURLToPath(import.meta.url));
|
|
1683
1936
|
for (;; ) {
|
|
1684
1937
|
const candidate = path2.join(dir, "package.json");
|
|
1685
|
-
if (
|
|
1686
|
-
const pkg = JSON.parse(
|
|
1938
|
+
if (existsSync3(candidate)) {
|
|
1939
|
+
const pkg = JSON.parse(readFileSync3(candidate, "utf8"));
|
|
1687
1940
|
if (pkg.name === "@tertium/gith")
|
|
1688
1941
|
return candidate;
|
|
1689
1942
|
}
|
|
@@ -1696,7 +1949,7 @@ function packageJsonPath() {
|
|
|
1696
1949
|
|
|
1697
1950
|
// src/components/version/version.ts
|
|
1698
1951
|
function readVersion() {
|
|
1699
|
-
const pkg = JSON.parse(
|
|
1952
|
+
const pkg = JSON.parse(readFileSync4(packageJsonPath(), "utf8"));
|
|
1700
1953
|
return pkg.version ?? "unknown";
|
|
1701
1954
|
}
|
|
1702
1955
|
function versionText() {
|
|
@@ -1710,21 +1963,27 @@ function versionText() {
|
|
|
1710
1963
|
var USAGE = `gith - git helper: branch flow and everyday git commands
|
|
1711
1964
|
|
|
1712
1965
|
Start and finish work
|
|
1713
|
-
gith create -b <branch> work branch from develop
|
|
1966
|
+
gith create -b <branch> work branch develop--<branch> from develop (no switch)
|
|
1714
1967
|
gith create -r <version> release/<version> from develop
|
|
1715
1968
|
gith create -h <version> hotfix/<version> from main
|
|
1716
1969
|
gith create --release | --hotfix version from the latest tag: minor for release, patch for hotfix
|
|
1717
|
-
gith switch
|
|
1970
|
+
gith switch list local and origin branches, and switch to the one picked
|
|
1971
|
+
gith switch -b <branch> switch to a branch, creating develop--<branch> from develop if missing (on a detached HEAD: <branch> from that commit)
|
|
1718
1972
|
gith switch -commit <sha> detach HEAD at a commit, to look at it (tracked files must be clean)
|
|
1973
|
+
gith switch --release | --hotfix switch to the newest release/* or hotfix/* (local or origin); -r / -h do the same; -r <v> / -h <v> for one version
|
|
1719
1974
|
gith update merge origin/develop into this work branch
|
|
1720
|
-
gith
|
|
1975
|
+
gith finish --current-release | --current-hotfix release the newest release/* or hotfix/*: version, changelog, merge to main, tag, back to develop
|
|
1976
|
+
gith merge --develop merge this branch into develop
|
|
1977
|
+
gith merge --main merge this branch into main
|
|
1978
|
+
gith merge pick the branch to merge, then the target (main, develop, release/*, hotfix/*)
|
|
1979
|
+
gith merge [-r <v> | -h <v>] merge this branch into release/<v> or hotfix/<v>; without a version, the only one or a pick from the list
|
|
1721
1980
|
gith merge --current-release merge this branch into the newest release (local or origin)
|
|
1722
1981
|
gith merge --current-hotfix merge this branch into the newest hotfix (local or origin)
|
|
1723
1982
|
gith release --patch | --minor | --major bump the version and publish
|
|
1724
1983
|
|
|
1725
1984
|
Fix history
|
|
1726
1985
|
gith undo undo the last commit, keep its changes staged (not if pushed)
|
|
1727
|
-
gith save [-m <msg>]
|
|
1986
|
+
gith save [-m <msg>] [--all] fold staged changes (--all stages every change first) into the last commit, or commit them if the branch has none yet (asks for a message); pushes if origin differs (asks before a force-push)
|
|
1728
1987
|
gith squash -limit <n> [-m <msg>] combine the last n commits into one (keeps the oldest message)
|
|
1729
1988
|
|
|
1730
1989
|
Save and reset work
|
|
@@ -1737,7 +1996,8 @@ Clean up branches (every removal shows what it will remove first)
|
|
|
1737
1996
|
gith remove -b <branch> delete the branch here and on origin (either side may be missing)
|
|
1738
1997
|
gith remove -b <branch> --local delete only the local branch
|
|
1739
1998
|
gith remove -b <branch> --remote delete only the origin branch
|
|
1740
|
-
gith remove
|
|
1999
|
+
gith remove pick branches to delete from a list (here and on origin)
|
|
2000
|
+
gith remove --branch [-l <n>] same list, for branch pairs (here and on origin)
|
|
1741
2001
|
gith remove --branch --local pick from local branches
|
|
1742
2002
|
gith remove --branch --remote pick from origin branches
|
|
1743
2003
|
gith remove --obsolete [--local] [-days <n>] remove branches untouched for 90 days, after Y/n
|
|
@@ -1794,11 +2054,14 @@ try {
|
|
|
1794
2054
|
case "create":
|
|
1795
2055
|
create(parseFlags(rest));
|
|
1796
2056
|
break;
|
|
2057
|
+
case "finish":
|
|
2058
|
+
await finish(parseFlags(rest));
|
|
2059
|
+
break;
|
|
1797
2060
|
case "merge":
|
|
1798
|
-
merge(parseFlags(rest));
|
|
2061
|
+
await merge(parseFlags(rest));
|
|
1799
2062
|
break;
|
|
1800
2063
|
case "switch":
|
|
1801
|
-
switchBranch(parseFlags(rest));
|
|
2064
|
+
await switchBranch(parseFlags(rest));
|
|
1802
2065
|
break;
|
|
1803
2066
|
case "remove":
|
|
1804
2067
|
case "rm":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tertium/gith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Git helper: branch flow with goal branches, release and hotfix containers, and everyday git commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
"test": "bun test --no-coverage",
|
|
16
16
|
"lint": "biome check src",
|
|
17
17
|
"lint:fix": "biome check --write src",
|
|
18
|
+
"format": "biome format --write src",
|
|
19
|
+
"format:check": "biome format src",
|
|
18
20
|
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepare": "git config core.hooksPath .githooks || true",
|
|
19
22
|
"release:patch": "node bin/gith.js release --patch",
|
|
20
23
|
"release:minor": "node bin/gith.js release --minor",
|
|
21
24
|
"release:major": "node bin/gith.js release --major",
|