@tertium/gith 0.3.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 +13 -7
- package/bin/gith.js +67 -31
- 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,17 +26,19 @@ 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
40
|
gith switch # list local and origin branches, and switch to the one you pick
|
|
39
|
-
gith switch -b login-redirect # switch to
|
|
41
|
+
gith switch -b login-redirect # switch to develop--login-redirect, creating it from develop if it is missing
|
|
40
42
|
gith switch -commit 3f2a1b9 # detach HEAD at that commit, to look at old code
|
|
41
43
|
gith switch --release # switch to the newest release/* (local or origin); -r 1.9.0 for one version
|
|
42
44
|
gith switch --hotfix # switch to the newest hotfix/* (local or origin); -h 1.2.1 for one version
|
|
@@ -45,7 +47,8 @@ gith merge # this branch into develop
|
|
|
45
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
|
|
46
48
|
gith finish --current-hotfix # the same for the newest hotfix/*
|
|
47
49
|
gith merge # pick the branch to merge, then the target from the list
|
|
48
|
-
gith merge
|
|
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)
|
|
49
52
|
gith merge -h 1.2.1 # this branch into hotfix/1.2.1
|
|
50
53
|
gith merge --current-release # this branch into the newest release/* (local or origin)
|
|
51
54
|
gith merge --current-hotfix # this branch into the newest hotfix/* (local or origin)
|
|
@@ -55,7 +58,7 @@ gith release --major # npm version major, from develop into main
|
|
|
55
58
|
|
|
56
59
|
# Fix history
|
|
57
60
|
gith undo # undo the last commit, keep its changes staged (not if pushed)
|
|
58
|
-
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
|
|
59
62
|
gith squash -limit 3 [-m "msg"] # combine the last 3 commits into one (keeps the oldest message)
|
|
60
63
|
|
|
61
64
|
# Save and reset work
|
|
@@ -120,9 +123,12 @@ One rule applies to every command: **a single dash always takes a value, and a d
|
|
|
120
123
|
| `-b <name>`, `-branch <name>` | value | `create`, `switch`, `remove` |
|
|
121
124
|
| `--branch` | alone: open the picker | `remove` |
|
|
122
125
|
| `-r <version>`, `-release <version>` | value | `create`, `merge` |
|
|
126
|
+
| `-r` alone | the only release, or a pick from the list | `merge` |
|
|
123
127
|
| `--release` | alone: version from the latest tag | `create` |
|
|
128
|
+
| `--main` | alone: merge into main | `merge` |
|
|
124
129
|
| `--current-release`, `--current-hotfix` | switch: merge into the newest release or hotfix | `merge` |
|
|
125
130
|
| `-h <version>`, `-hotfix <version>` | value | `create`, `merge` |
|
|
131
|
+
| `-h` alone | the only hotfix, or a pick from the list | `merge` |
|
|
126
132
|
| `--hotfix` | alone: version from the latest tag | `create` |
|
|
127
133
|
| `-limit <n>` | value | `squash`: how many commits to combine |
|
|
128
134
|
| `-m <msg>`, `-message <msg>` | value | `squash` |
|
|
@@ -145,7 +151,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
145
151
|
|
|
146
152
|
## Git commands
|
|
147
153
|
|
|
148
|
-
- **`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.
|
|
149
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.
|
|
150
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.
|
|
151
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.
|
|
@@ -179,7 +185,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
179
185
|
|
|
180
186
|
## What it does
|
|
181
187
|
|
|
182
|
-
- **`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.
|
|
183
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.
|
|
184
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.
|
|
185
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.
|
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"],
|
|
@@ -29,6 +29,7 @@ var SPECS = new Map([
|
|
|
29
29
|
["current-release", "switch"],
|
|
30
30
|
["current-hotfix", "switch"],
|
|
31
31
|
["develop", "switch"],
|
|
32
|
+
["main", "switch"],
|
|
32
33
|
["pockets", "switch"],
|
|
33
34
|
["detached", "switch"],
|
|
34
35
|
["all", "switch"],
|
|
@@ -89,10 +90,14 @@ function parseFlags(argv) {
|
|
|
89
90
|
if (style === "single") {
|
|
90
91
|
if (kind === "switch")
|
|
91
92
|
throw new Error(`${token} is a switch; write --${name}.`);
|
|
92
|
-
if (
|
|
93
|
+
if (hasNext) {
|
|
94
|
+
flags.set(name, next);
|
|
95
|
+
i++;
|
|
96
|
+
} else if (kind === "optional") {
|
|
97
|
+
flags.set(name, "");
|
|
98
|
+
} else {
|
|
93
99
|
throw new Error(`${token} needs a value.`);
|
|
94
|
-
|
|
95
|
-
i++;
|
|
100
|
+
}
|
|
96
101
|
} else if (kind === "switch") {
|
|
97
102
|
flags.set(name, "true");
|
|
98
103
|
} else if (kind === "value") {
|
|
@@ -548,7 +553,7 @@ async function select(question, options, multi) {
|
|
|
548
553
|
|
|
549
554
|
// src/components/rules/rules.constants.ts
|
|
550
555
|
var CONTAINER = /^(develop|main|release\/.+|hotfix\/.+)$/;
|
|
551
|
-
var TARGET = /^(develop|release\/.+|hotfix\/.+)$/;
|
|
556
|
+
var TARGET = /^(main|develop|release\/.+|hotfix\/.+)$/;
|
|
552
557
|
|
|
553
558
|
// src/components/rules/rules.ts
|
|
554
559
|
function isContainer(name) {
|
|
@@ -597,7 +602,7 @@ function nextVersion(tags, bump) {
|
|
|
597
602
|
}
|
|
598
603
|
|
|
599
604
|
// src/components/flow/flow.utils.ts
|
|
600
|
-
function
|
|
605
|
+
function containers(kind) {
|
|
601
606
|
const prefix = `${kind}/`;
|
|
602
607
|
const local = captureArgs([
|
|
603
608
|
"for-each-ref",
|
|
@@ -608,15 +613,19 @@ function newestContainer(kind) {
|
|
|
608
613
|
const origin = captureArgs(["ls-remote", "--heads", "origin"]).split(`
|
|
609
614
|
`).map((line) => (line.split("\t")[1] ?? "").replace(/^refs\/heads\//, ""));
|
|
610
615
|
const candidates = new Set([...local, ...origin].filter((name) => name.startsWith(prefix)));
|
|
611
|
-
|
|
616
|
+
const versioned = [];
|
|
612
617
|
for (const name of candidates) {
|
|
613
618
|
const version = parseSemver(name.slice(prefix.length));
|
|
614
|
-
if (version
|
|
615
|
-
|
|
619
|
+
if (version)
|
|
620
|
+
versioned.push({ name, version });
|
|
616
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);
|
|
617
626
|
if (!newest)
|
|
618
627
|
throw new Error(`no ${kind} branch exists, locally or on origin.`);
|
|
619
|
-
return newest
|
|
628
|
+
return newest;
|
|
620
629
|
}
|
|
621
630
|
function nextContainerVersion(kind) {
|
|
622
631
|
const bump = kind === "release" ? "minor" : "patch";
|
|
@@ -658,6 +667,7 @@ async function merge(flags) {
|
|
|
658
667
|
return mergePick();
|
|
659
668
|
const kind = onlyOne(flags, [
|
|
660
669
|
"develop",
|
|
670
|
+
"main",
|
|
661
671
|
"release",
|
|
662
672
|
"hotfix",
|
|
663
673
|
"current-release",
|
|
@@ -666,31 +676,50 @@ async function merge(flags) {
|
|
|
666
676
|
let target = "develop";
|
|
667
677
|
if (kind === "develop")
|
|
668
678
|
target = "develop";
|
|
679
|
+
else if (kind === "main")
|
|
680
|
+
target = "main";
|
|
669
681
|
else if (kind === "current-release")
|
|
670
682
|
target = newestContainer("release");
|
|
671
683
|
else if (kind === "current-hotfix")
|
|
672
684
|
target = newestContainer("hotfix");
|
|
673
685
|
else if (kind) {
|
|
674
686
|
const version = flags.get(kind) ?? "";
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
target =
|
|
687
|
+
const picked = version === "" ? await pickContainer(kind) : containerName(kind, version);
|
|
688
|
+
if (picked === undefined)
|
|
689
|
+
return;
|
|
690
|
+
target = picked;
|
|
679
691
|
}
|
|
680
692
|
await mergeInto(target);
|
|
681
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;
|
|
708
|
+
}
|
|
682
709
|
function createBranch(goal, from) {
|
|
683
710
|
if (isContainer(goal))
|
|
684
711
|
throw new Error(`'${goal}' is a container name; pick a goal.`);
|
|
685
|
-
|
|
686
|
-
|
|
712
|
+
const name = workBranchName(goal, from);
|
|
713
|
+
if (hasOnOrigin(name))
|
|
714
|
+
throw new Error(`'${name}' already exists on origin.`);
|
|
687
715
|
if (from === undefined) {
|
|
688
716
|
gitArgs(["fetch", "origin", "develop"]);
|
|
689
|
-
gitArgs(["branch",
|
|
717
|
+
gitArgs(["branch", name, "origin/develop"]);
|
|
690
718
|
} else {
|
|
691
|
-
gitArgs(["branch",
|
|
719
|
+
gitArgs(["branch", name, from]);
|
|
692
720
|
}
|
|
693
|
-
gitArgs(["push", "-u", "origin",
|
|
721
|
+
gitArgs(["push", "-u", "origin", name]);
|
|
722
|
+
return name;
|
|
694
723
|
}
|
|
695
724
|
async function mergeInto(target, from) {
|
|
696
725
|
if (!isTarget(target))
|
|
@@ -889,10 +918,12 @@ function undo(flags) {
|
|
|
889
918
|
}
|
|
890
919
|
async function save(flags) {
|
|
891
920
|
const message = flags.get("message");
|
|
892
|
-
const extra = [...flags.keys()].filter((key) => key !== "message");
|
|
921
|
+
const extra = [...flags.keys()].filter((key) => key !== "message" && key !== "all");
|
|
893
922
|
if (extra.length > 0)
|
|
894
|
-
throw new Error("usage: gith save [-m <message>]");
|
|
923
|
+
throw new Error("usage: gith save [-m <message>] [--all]");
|
|
895
924
|
const branch = currentBranch();
|
|
925
|
+
if (flags.has("all"))
|
|
926
|
+
gitArgs(["add", "-A"]);
|
|
896
927
|
if (captureArgs(["diff", "--cached", "--name-only"]) === "") {
|
|
897
928
|
throw new Error("nothing staged; stage the changes to add to the last commit.");
|
|
898
929
|
}
|
|
@@ -1163,10 +1194,14 @@ async function switchBranch(flags) {
|
|
|
1163
1194
|
return;
|
|
1164
1195
|
}
|
|
1165
1196
|
const branch = requireBranch(flags, "switch");
|
|
1166
|
-
|
|
1167
|
-
|
|
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);
|
|
1168
1203
|
}
|
|
1169
|
-
gitArgs(["switch", "--",
|
|
1204
|
+
gitArgs(["switch", "--", name]);
|
|
1170
1205
|
}
|
|
1171
1206
|
function candidates(scope, local, remote, current) {
|
|
1172
1207
|
const notContainer = (name) => !isContainer(name);
|
|
@@ -1928,26 +1963,27 @@ function versionText() {
|
|
|
1928
1963
|
var USAGE = `gith - git helper: branch flow and everyday git commands
|
|
1929
1964
|
|
|
1930
1965
|
Start and finish work
|
|
1931
|
-
gith create -b <branch> work branch from develop
|
|
1966
|
+
gith create -b <branch> work branch develop--<branch> from develop (no switch)
|
|
1932
1967
|
gith create -r <version> release/<version> from develop
|
|
1933
1968
|
gith create -h <version> hotfix/<version> from main
|
|
1934
1969
|
gith create --release | --hotfix version from the latest tag: minor for release, patch for hotfix
|
|
1935
1970
|
gith switch list local and origin branches, and switch to the one picked
|
|
1936
|
-
gith switch -b <branch> switch to a branch, creating
|
|
1971
|
+
gith switch -b <branch> switch to a branch, creating develop--<branch> from develop if missing (on a detached HEAD: <branch> from that commit)
|
|
1937
1972
|
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
|
|
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
|
|
1939
1974
|
gith update merge origin/develop into this work branch
|
|
1940
1975
|
gith finish --current-release | --current-hotfix release the newest release/* or hotfix/*: version, changelog, merge to main, tag, back to develop
|
|
1941
1976
|
gith merge --develop merge this branch into develop
|
|
1942
|
-
gith merge
|
|
1943
|
-
gith merge
|
|
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
|
|
1944
1980
|
gith merge --current-release merge this branch into the newest release (local or origin)
|
|
1945
1981
|
gith merge --current-hotfix merge this branch into the newest hotfix (local or origin)
|
|
1946
1982
|
gith release --patch | --minor | --major bump the version and publish
|
|
1947
1983
|
|
|
1948
1984
|
Fix history
|
|
1949
1985
|
gith undo undo the last commit, keep its changes staged (not if pushed)
|
|
1950
|
-
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)
|
|
1951
1987
|
gith squash -limit <n> [-m <msg>] combine the last n commits into one (keeps the oldest message)
|
|
1952
1988
|
|
|
1953
1989
|
Save and reset work
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tertium/gith",
|
|
3
|
-
"version": "0.3.
|
|
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",
|