@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.
Files changed (3) hide show
  1. package/README.md +13 -7
  2. package/bin/gith.js +67 -31
  3. 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) (goal-named work branches, `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).
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, named by its goal; stays on the current branch
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 the branch, creating it from develop if it is missing
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 -r 1.2.0 # this branch into release/1.2.0
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"] # 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
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 the branch exists neither locally nor on origin, it is created first, the same way as `create -b`, and then checked out.
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 the branch from `origin/develop`, pushes it with upstream tracking, and leaves the current branch checked out. It does not touch the working tree, so it works with uncommitted changes.
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", "auto"],
10
- ["hotfix", "auto"],
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 (!hasNext)
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
- flags.set(name, next);
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 newestContainer(kind) {
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
- let newest;
616
+ const versioned = [];
612
617
  for (const name of candidates) {
613
618
  const version = parseSemver(name.slice(prefix.length));
614
- if (version && (!newest || compare(version, newest.version) > 0))
615
- newest = { name, version };
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.name;
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
- if (!version) {
676
- throw new Error(`usage: gith merge -${kind === "release" ? "r" : "h"} <version>`);
677
- }
678
- target = containerName(kind, version);
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
- if (hasOnOrigin(goal))
686
- throw new Error(`'${goal}' already exists on origin.`);
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", goal, "origin/develop"]);
717
+ gitArgs(["branch", name, "origin/develop"]);
690
718
  } else {
691
- gitArgs(["branch", goal, from]);
719
+ gitArgs(["branch", name, from]);
692
720
  }
693
- gitArgs(["push", "-u", "origin", goal]);
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
- if (!hasLocal(branch) && !hasOnOrigin(branch)) {
1167
- createBranch(branch, isDetached() ? "HEAD" : undefined);
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", "--", branch]);
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, named by its goal (no switch)
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 it from develop if missing (on a detached HEAD: from that commit)
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 pick the branch to merge, then the target (develop, release/*, hotfix/*)
1943
- gith merge [-r <v> | -h <v>] merge this branch into develop, release/<v> or hotfix/<v>
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>] fold staged changes 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)
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.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",