@tertium/gith 0.4.0 → 0.5.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 +33 -16
- package/bin/gith.js +170 -79
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
# @tertium/gith
|
|
2
2
|
|
|
3
|
+
<img src="assets/gith-icon.svg" alt="gith mascot with three commit sprouts" width="128" />
|
|
4
|
+
|
|
3
5
|
gith: git-only branch flow CLI (goal branches, release/hotfix containers, synced merges).
|
|
4
6
|
|
|
5
7
|
```
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
o----o----o
|
|
8
|
+
o
|
|
9
|
+
o | o
|
|
9
10
|
\ | /
|
|
10
11
|
\ | /
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
\ | /
|
|
13
|
+
\|/
|
|
14
|
+
|
|
|
15
|
+
.---.
|
|
16
|
+
( o o ) gith
|
|
17
|
+
'---' git happens.
|
|
18
|
+
|
|
|
19
|
+
/|\
|
|
20
|
+
/ \
|
|
14
21
|
```
|
|
15
22
|
|
|
16
23
|
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, push, pull, 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).
|
|
17
24
|
|
|
18
25
|
See [Command design rules](docs/RULES.md) before changing or adding commands and flags.
|
|
19
26
|
|
|
27
|
+
See [CHANGELOG.md](CHANGELOG.md) for user-facing release notes and the changes included in each version. GitHub Releases are also published for tagged versions.
|
|
28
|
+
|
|
20
29
|
> **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.
|
|
21
30
|
|
|
22
31
|
## Usage
|
|
@@ -28,6 +37,8 @@ gith create -r 1.2.0 # release/1.2.0 from develop
|
|
|
28
37
|
gith create -h 1.2.1 # hotfix/1.2.1 from main
|
|
29
38
|
gith create --release # next release version from the latest tag
|
|
30
39
|
gith create --hotfix # next hotfix version from the latest tag
|
|
40
|
+
gith create -r 1.2.0 --update-version --npm # also set npm version to 1.2.0 on the new release branch
|
|
41
|
+
gith create --hotfix --update-version --npm # same for an automatically selected hotfix version
|
|
31
42
|
gith switch # list local and origin branches, and switch to the one you pick
|
|
32
43
|
gith switch -b login-redirect # switch to develop--login-redirect, creating it from develop if it is missing
|
|
33
44
|
gith switch -commit 3f2a1b9 # detach HEAD at that commit, to look at old code
|
|
@@ -44,9 +55,9 @@ gith merge --release # this branch into the only release, or pick
|
|
|
44
55
|
gith merge -h 1.2.1 # this branch into hotfix/1.2.1
|
|
45
56
|
gith merge --current-release # this branch into the newest release/* (local or origin)
|
|
46
57
|
gith merge --current-hotfix # this branch into the newest hotfix/* (local or origin)
|
|
47
|
-
gith release --patch #
|
|
48
|
-
gith release --minor #
|
|
49
|
-
gith release --major
|
|
58
|
+
gith release --patch # sync main into develop without changing package versions
|
|
59
|
+
gith release --minor # merge develop into main and sync back
|
|
60
|
+
gith release --major --update-version --npm # bump npm major version and release through main
|
|
50
61
|
|
|
51
62
|
# Fix history
|
|
52
63
|
gith undo # undo the last commit, keep its changes staged (not if pushed)
|
|
@@ -75,14 +86,14 @@ gith remove --detached # pick local branches that have no origin co
|
|
|
75
86
|
gith remove --detached --all # remove all of them: lists them, then asks Y/n
|
|
76
87
|
|
|
77
88
|
# Look at the repo
|
|
78
|
-
gith status #
|
|
89
|
+
gith status # working-tree summary, all local and remote-tracking branches
|
|
79
90
|
gith list --commits # the last 5 commits on this branch
|
|
80
91
|
gith list --commits --graph # the same, as a branch graph
|
|
81
92
|
gith list --commits -l 10 # the last 10 (-limit 10 works too)
|
|
82
93
|
gith list --tags # the newest 5 tags with a LOCAL and ORIGIN column
|
|
83
94
|
gith list --tags -l 20 # the newest 20
|
|
84
95
|
gith list --stashes # the newest 5 stashes (-l for more)
|
|
85
|
-
gith show --pockets # the same, with
|
|
96
|
+
gith show --pockets # the same, with the gith mascot: my precious pockets!
|
|
86
97
|
gith find -commit "login" -limit 10 # commits whose message mentions login (search is the same; default 5)
|
|
87
98
|
gith search -file "*api.ts" -limit 3 # commits that changed a matching file, with the files listed
|
|
88
99
|
gith find -change "TODO" # commits that added or removed the text in their diff
|
|
@@ -138,6 +149,7 @@ One rule applies to every command: **a single dash always takes a value, and a d
|
|
|
138
149
|
| `-file <pattern>`, `-change <text>` | value | `find` |
|
|
139
150
|
| `-l <n>`, `-limit <n>` | value, positive whole number | every listing: `list`, `tree`, the picker |
|
|
140
151
|
| `--patch`, `--minor`, `--major` | switch | `release` |
|
|
152
|
+
| `--update-version`, `--npm` | switches, required together for npm version updates | `release`, `create` with `-r`/`-h`/`--release`/`--hotfix` |
|
|
141
153
|
| `--overall`, `--current-year`, `--current-month`, `--current-week`, `--last-year`, `--last-month`, `--last-week` | switch, one at a time | `stat` |
|
|
142
154
|
| `--local`, `--remote` | switch | `remove` (both sides when neither is given) |
|
|
143
155
|
| `--obsolete`, `--detached` | switch | `remove` |
|
|
@@ -160,7 +172,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
160
172
|
- **`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.
|
|
161
173
|
- **`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.
|
|
162
174
|
- **`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.
|
|
163
|
-
- **`status`** prints the branch and how far it is ahead of or behind its
|
|
175
|
+
- **`status`** prints the current branch and how far it is ahead of or behind its upstream, then counts staged, unstaged, untracked and conflicting files. It also lists all local branches (`*` marks the current branch; `+` marks branches in other worktrees) and remote-tracking branches from every configured remote. Remote branches reflect the last fetch; run `git fetch --all --prune` to refresh them. Empty lists are shown explicitly. Status changes nothing and does not contact remotes.
|
|
164
176
|
- **`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.
|
|
165
177
|
- **`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.
|
|
166
178
|
- **`push` / `pull`** work with the current branch and its same-named branch on origin. They report fresh ahead/behind counts and stop with instructions when histories diverge. Push sets upstream tracking and publishes missing branches; it never force-pushes. Pull requires clean tracked files, fast-forwards only, and points to `gith push` when local commits remain. `--all-branches` applies to every local branch, preflighting all before updating any. Push sends them atomically (origin must support atomic pushes); pull requires each to exist on origin and updates branches without switching. Branches checked out in other worktrees cannot be updated by pull. Remote-only branches are not created. Pull may leave earlier branches updated if a later Git operation fails; it never rolls back successful fast-forwards. Dry runs print actions without fetching or changing refs.
|
|
@@ -171,7 +183,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
171
183
|
- `-file <pattern>` matches the paths changed by the commit, using git's pattern rules (`*api.ts` matches `src/api.ts`). Each commit lists the matching files it changed.
|
|
172
184
|
- `-change <text>` matches commits whose diff adds or removes the text (`git log -S`). With `--regex`, it matches a regular expression against the diff (`git log -G`).
|
|
173
185
|
- `-limit <n>` sets how many to show (default 5). The filters combine with AND.
|
|
174
|
-
- **`show`** is a synonym for `list`: `gith show --tags` and `gith list --tags` do the same. `--pockets` is a fun name for `--stashes`, and it prints
|
|
186
|
+
- **`show`** is a synonym for `list`: `gith show --tags` and `gith list --tags` do the same. `--pockets` is a fun name for `--stashes`, and it prints the gith mascot above the list, matching `gith --version` and the SVG/PNG logo in `assets/`.
|
|
175
187
|
- **`list --stashes`** shows the stashes, newest first, with their date and description. Like the other listings, it shows 5 by default, and `-limit` changes that.
|
|
176
188
|
- **`squash -limit <n>`** combines the last `n` commits on the current work branch into one, using `reset --soft` and a single commit. Without `-m` the result keeps the oldest of the combined commits, its message and author. It refuses when `n` is below 2, when `n` would reach the first commit, when a merge is among them, when any of them is already on origin, on a container, or with a dirty working tree.
|
|
177
189
|
- **`stat`** reads the history of the current branch and prints a table of facts: commit count, top author, the biggest commit (lines added and removed), average size, the most changed file, the busiest day, weekday and hour, the longest streak of days with a commit, night owl and weekend commits, and the conventional commit types. The period is one switch: `--overall` (the default), `--current-year`, `--current-month`, `--current-week` (weeks start on Monday), or the matching `--last-` switch for the previous year, month or week. `-limit <n>` looks only at the last `n` commits. Stat changes nothing.
|
|
@@ -195,6 +207,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
195
207
|
|
|
196
208
|
- **`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.
|
|
197
209
|
- **`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.
|
|
210
|
+
- **`create -r/-h/--release/--hotfix --update-version --npm`** also sets npm's version to the branch version and commits `package.json` and any npm lockfiles on that new branch before pushing. Both flags are required together and are rejected for work branches. This uses a temporary worktree, leaves your checked-out branch and local changes intact, and creates no version tag. npm lifecycle scripts are skipped; changelog preparation remains part of finishing the release. An already matching package version needs no commit. Without both flags, creation does not run npm or change package versions. The selected base must contain a `package.json` with a version. If npm or the commit fails, the new branch and temporary worktree remain available for inspection and are not pushed.
|
|
198
211
|
- **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.
|
|
199
212
|
- **`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.
|
|
200
213
|
- **`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.
|
|
@@ -207,10 +220,14 @@ Containers (`develop`, `main`, `release/*`, `hotfix/*`) can't be merged from, an
|
|
|
207
220
|
|
|
208
221
|
Run as `gith release --patch`, `--minor` or `--major`.
|
|
209
222
|
|
|
210
|
-
- **`patch`** runs from `main`:
|
|
211
|
-
- **`minor` and `major`** run from `develop`:
|
|
223
|
+
- **`patch`** runs from `main`: pushes with tags, then rebases `develop` onto `main`. The push back to `develop` uses `--force-with-lease`, so it refuses to overwrite a `develop` that moved on origin.
|
|
224
|
+
- **`minor` and `major`** run from `develop`: push with tags, merge `develop` into `main`, then bring `main` back to `develop`.
|
|
225
|
+
|
|
226
|
+
Both need a clean working tree. By default, they perform only the Git flow: no package manager runs, no package files change, and no new version tag is created. Existing annotated tags reachable from pushed commits can still be pushed.
|
|
227
|
+
|
|
228
|
+
Add **both `--update-version --npm`** to run `npm version` with the selected patch, minor, or major bump before pushing. This requires npm and a `package.json` with a version; npm lifecycle scripts run as usual. Either flag alone is an error. npm is currently the only supported versioning tool. This repository's `release:patch`, `release:minor`, and `release:major` scripts pass both flags explicitly.
|
|
212
229
|
|
|
213
|
-
|
|
230
|
+
Pushing a `v*` tag publishes the package and creates a GitHub Release with generated notes.
|
|
214
231
|
|
|
215
232
|
## Errors
|
|
216
233
|
|
package/bin/gith.js
CHANGED
|
@@ -19,6 +19,8 @@ var SPECS = new Map([
|
|
|
19
19
|
["patch", "switch"],
|
|
20
20
|
["minor", "switch"],
|
|
21
21
|
["major", "switch"],
|
|
22
|
+
["update-version", "switch"],
|
|
23
|
+
["npm", "switch"],
|
|
22
24
|
["remote", "switch"],
|
|
23
25
|
["local", "switch"],
|
|
24
26
|
["tags", "switch"],
|
|
@@ -449,7 +451,7 @@ function find(flags) {
|
|
|
449
451
|
|
|
450
452
|
// src/components/flow/flow.ts
|
|
451
453
|
import { execSync } from "node:child_process";
|
|
452
|
-
import { existsSync as
|
|
454
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
453
455
|
|
|
454
456
|
// src/components/prompt/prompt.ts
|
|
455
457
|
import { emitKeypressEvents } from "node:readline";
|
|
@@ -604,6 +606,16 @@ function nextVersion(tags, bump) {
|
|
|
604
606
|
}
|
|
605
607
|
|
|
606
608
|
// src/components/flow/flow.utils.ts
|
|
609
|
+
import { spawnSync } from "node:child_process";
|
|
610
|
+
import {
|
|
611
|
+
existsSync as existsSync2,
|
|
612
|
+
mkdtempSync,
|
|
613
|
+
realpathSync,
|
|
614
|
+
symlinkSync,
|
|
615
|
+
unlinkSync
|
|
616
|
+
} from "node:fs";
|
|
617
|
+
import { tmpdir } from "node:os";
|
|
618
|
+
import path2 from "node:path";
|
|
607
619
|
function containers(kind) {
|
|
608
620
|
const prefix = `${kind}/`;
|
|
609
621
|
const local = captureArgs([
|
|
@@ -640,31 +652,77 @@ function nextContainerVersion(kind) {
|
|
|
640
652
|
console.log(`Next ${kind} version from existing tags: ${version}`);
|
|
641
653
|
return version;
|
|
642
654
|
}
|
|
643
|
-
|
|
655
|
+
var open = (kind, version, shouldUpdateVersion) => {
|
|
656
|
+
if (shouldUpdateVersion && !/^\d+\.\d+\.\d+$/.test(version))
|
|
657
|
+
throw new Error("npm version updates require a version in X.Y.Z format; use -r or -h with that version.");
|
|
644
658
|
const name = containerName(kind, version);
|
|
659
|
+
assertBranchName(name);
|
|
660
|
+
if (hasLocal(name))
|
|
661
|
+
throw new Error(`'${name}' already exists locally.`);
|
|
645
662
|
if (hasOnOrigin(name))
|
|
646
663
|
throw new Error(`'${name}' already exists on origin.`);
|
|
647
664
|
const base = containerBase(kind);
|
|
648
665
|
gitArgs(["fetch", "origin", base]);
|
|
666
|
+
const needsVersionUpdate = shouldUpdateVersion && parsePackageVersion(captureArgs(["show", `origin/${base}:package.json`])) !== version;
|
|
649
667
|
gitArgs(["branch", name, `origin/${base}`]);
|
|
668
|
+
if (needsVersionUpdate)
|
|
669
|
+
updateContainerVersion(name, version);
|
|
650
670
|
gitArgs(["push", "-u", "origin", name]);
|
|
651
|
-
}
|
|
671
|
+
};
|
|
672
|
+
var updateContainerVersion = (name, version) => {
|
|
673
|
+
if (isDryRun()) {
|
|
674
|
+
process.stdout.write(`> npm version ${version} --no-git-tag-version --ignore-scripts (temporary worktree for ${name})
|
|
675
|
+
`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const cwd = mkdtempSync(path2.join(tmpdir(), "gith-version-"));
|
|
679
|
+
gitArgs(["worktree", "add", cwd, name]);
|
|
680
|
+
const hasDependencies = existsSync2("node_modules");
|
|
681
|
+
if (hasDependencies)
|
|
682
|
+
symlinkSync(realpathSync("node_modules"), path2.join(cwd, "node_modules"), "junction");
|
|
683
|
+
try {
|
|
684
|
+
commitContainerVersion(cwd, version);
|
|
685
|
+
} finally {
|
|
686
|
+
if (hasDependencies)
|
|
687
|
+
unlinkSync(path2.join(cwd, "node_modules"));
|
|
688
|
+
}
|
|
689
|
+
gitArgs(["worktree", "remove", cwd]);
|
|
690
|
+
};
|
|
691
|
+
var commitContainerVersion = (cwd, version) => {
|
|
692
|
+
const result = spawnSync("npm", ["version", version, "--no-git-tag-version", "--ignore-scripts"], {
|
|
693
|
+
cwd,
|
|
694
|
+
stdio: "inherit",
|
|
695
|
+
shell: process.platform === "win32"
|
|
696
|
+
});
|
|
697
|
+
if (result.error || result.status !== 0)
|
|
698
|
+
throw new Error(`npm version failed; inspect the preserved worktree at ${cwd} and retry.`, { cause: result.error });
|
|
699
|
+
gitArgs([
|
|
700
|
+
"add",
|
|
701
|
+
"--",
|
|
702
|
+
...["package.json", "package-lock.json", "npm-shrinkwrap.json"].filter((file) => existsSync2(path2.join(cwd, file)))
|
|
703
|
+
], { cwd });
|
|
704
|
+
gitArgs(["commit", "-m", version], { cwd });
|
|
705
|
+
};
|
|
652
706
|
|
|
653
707
|
// src/components/flow/flow.ts
|
|
654
|
-
|
|
655
|
-
assertAllowedFlags(flags, ["branch", "release", "hotfix"], "create");
|
|
708
|
+
var create = (flags) => {
|
|
709
|
+
assertAllowedFlags(flags, ["branch", "release", "hotfix", "update-version", "npm"], "create");
|
|
710
|
+
if (flags.has("update-version") !== flags.has("npm"))
|
|
711
|
+
throw new Error("Version updates require both --update-version and --npm; pass both flags or omit both.");
|
|
656
712
|
const kind = onlyOne(flags, ["branch", "release", "hotfix"]);
|
|
657
713
|
if (!kind)
|
|
658
|
-
throw new Error("usage: gith create -b <branch> | -r <version> | -h <version> | --release | --hotfix");
|
|
714
|
+
throw new Error("usage: gith create -b <branch> | (-r <version> | -h <version> | --release | --hotfix) [--update-version --npm]");
|
|
659
715
|
const value = flags.get(kind) ?? "";
|
|
660
716
|
if (kind === "branch") {
|
|
717
|
+
if (flags.has("update-version"))
|
|
718
|
+
throw new Error("--update-version --npm only applies to release or hotfix branch creation.");
|
|
661
719
|
if (!value)
|
|
662
720
|
throw new Error("usage: gith create -b <branch>");
|
|
663
721
|
createBranch(value);
|
|
664
722
|
return;
|
|
665
723
|
}
|
|
666
|
-
open(kind, value || nextContainerVersion(kind));
|
|
667
|
-
}
|
|
724
|
+
open(kind, value || nextContainerVersion(kind), flags.has("update-version"));
|
|
725
|
+
};
|
|
668
726
|
async function merge(flags) {
|
|
669
727
|
assertAllowedFlags(flags, [
|
|
670
728
|
"develop",
|
|
@@ -819,7 +877,7 @@ async function mergePick() {
|
|
|
819
877
|
await mergeInto(target, source);
|
|
820
878
|
}
|
|
821
879
|
function alignVersion(version) {
|
|
822
|
-
if (!
|
|
880
|
+
if (!existsSync3("package.json"))
|
|
823
881
|
return;
|
|
824
882
|
const current = parsePackageVersion(readFileSync2("package.json", "utf8"));
|
|
825
883
|
if (current === version)
|
|
@@ -835,10 +893,10 @@ function alignVersion(version) {
|
|
|
835
893
|
gitArgs(["commit", "-m", version, "--", "package.json"]);
|
|
836
894
|
}
|
|
837
895
|
function addChangelogEntry(version) {
|
|
838
|
-
if (!
|
|
896
|
+
if (!existsSync3("package.json"))
|
|
839
897
|
return;
|
|
840
898
|
const file = "CHANGELOG.md";
|
|
841
|
-
if (
|
|
899
|
+
if (existsSync3(file) && readFileSync2(file, "utf8").includes(`## ${version} - `))
|
|
842
900
|
return;
|
|
843
901
|
changelog(new Map);
|
|
844
902
|
}
|
|
@@ -1090,6 +1148,23 @@ async function squash(flags) {
|
|
|
1090
1148
|
gitArgs(["commit", "-C", oldest]);
|
|
1091
1149
|
}
|
|
1092
1150
|
|
|
1151
|
+
// src/components/version/version.constants.ts
|
|
1152
|
+
var MASCOT = [
|
|
1153
|
+
" o",
|
|
1154
|
+
" o | o",
|
|
1155
|
+
" \\ | /",
|
|
1156
|
+
" \\ | /",
|
|
1157
|
+
" \\ | /",
|
|
1158
|
+
" \\|/",
|
|
1159
|
+
" |",
|
|
1160
|
+
" .---.",
|
|
1161
|
+
" ( o o )",
|
|
1162
|
+
" '---'",
|
|
1163
|
+
" |",
|
|
1164
|
+
" /|\\",
|
|
1165
|
+
" / \\"
|
|
1166
|
+
];
|
|
1167
|
+
|
|
1093
1168
|
// src/components/list/list.constants.ts
|
|
1094
1169
|
var DEFAULT_LIMIT2 = 5;
|
|
1095
1170
|
var GRAPH_ARGS = [
|
|
@@ -1102,13 +1177,6 @@ var GRAPH_ARGS = [
|
|
|
1102
1177
|
"--date=format:%Y-%m-%d %H:%M",
|
|
1103
1178
|
"--format=%C(bold yellow)%h%C(reset) %C(bold)%s%C(reset) %C(auto)%d%C(reset)%n%C(dim) └─ %an · %ad%C(reset)%n"
|
|
1104
1179
|
];
|
|
1105
|
-
var GORLUM = [
|
|
1106
|
-
' .-"""-.',
|
|
1107
|
-
" / o o \\",
|
|
1108
|
-
" | ___ | my precious pockets!",
|
|
1109
|
-
" \\ (___) /",
|
|
1110
|
-
" '-._.-'"
|
|
1111
|
-
];
|
|
1112
1180
|
|
|
1113
1181
|
// src/components/list/list.utils.ts
|
|
1114
1182
|
function yesNo(value) {
|
|
@@ -1192,10 +1260,12 @@ function localTags() {
|
|
|
1192
1260
|
return captureArgs(["tag", "--list"]).split(`
|
|
1193
1261
|
`).filter(Boolean);
|
|
1194
1262
|
}
|
|
1195
|
-
|
|
1196
|
-
if (
|
|
1197
|
-
|
|
1198
|
-
`)
|
|
1263
|
+
var stashes = (limit, withMascot) => {
|
|
1264
|
+
if (withMascot)
|
|
1265
|
+
process.stdout.write(`${MASCOT.join(`
|
|
1266
|
+
`)}
|
|
1267
|
+
my precious pockets!
|
|
1268
|
+
`);
|
|
1199
1269
|
const rows = captureArgs([
|
|
1200
1270
|
"stash",
|
|
1201
1271
|
"list",
|
|
@@ -1204,7 +1274,9 @@ function stashes(limit, withGorlum) {
|
|
|
1204
1274
|
]).split(`
|
|
1205
1275
|
`).filter(Boolean);
|
|
1206
1276
|
if (rows.length === 0) {
|
|
1207
|
-
|
|
1277
|
+
process.stdout.write(withMascot ? `There is nothing in my pockets!
|
|
1278
|
+
` : `No stashes.
|
|
1279
|
+
`);
|
|
1208
1280
|
return;
|
|
1209
1281
|
}
|
|
1210
1282
|
const shown = rows.slice(0, limit);
|
|
@@ -1213,7 +1285,7 @@ function stashes(limit, withGorlum) {
|
|
|
1213
1285
|
if (shown.length < rows.length) {
|
|
1214
1286
|
console.log(`showing ${shown.length} of ${rows.length}; -limit <n> shows more`);
|
|
1215
1287
|
}
|
|
1216
|
-
}
|
|
1288
|
+
};
|
|
1217
1289
|
function tags(limit) {
|
|
1218
1290
|
const pairs = tagPairs(localTags(), originTags());
|
|
1219
1291
|
if (pairs.length === 0) {
|
|
@@ -1691,23 +1763,26 @@ function npmVersion(kind) {
|
|
|
1691
1763
|
}
|
|
1692
1764
|
|
|
1693
1765
|
// src/components/release/release.ts
|
|
1694
|
-
|
|
1766
|
+
var release = (kind, flags) => {
|
|
1695
1767
|
if (!kind || !KINDS.includes(kind))
|
|
1696
|
-
throw new Error("usage: gith release
|
|
1768
|
+
throw new Error("usage: gith release --patch | --minor | --major [--update-version --npm]");
|
|
1769
|
+
if (flags.has("update-version") !== flags.has("npm"))
|
|
1770
|
+
throw new Error("Version updates require both --update-version and --npm; pass both flags or omit both.");
|
|
1697
1771
|
assertCleanTree();
|
|
1698
1772
|
if (kind === "patch") {
|
|
1699
|
-
releasePatch();
|
|
1773
|
+
releasePatch(flags.has("update-version"));
|
|
1700
1774
|
} else {
|
|
1701
|
-
releaseFromDevelop(kind);
|
|
1775
|
+
releaseFromDevelop(kind, flags.has("update-version"));
|
|
1702
1776
|
}
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1777
|
+
};
|
|
1778
|
+
var releasePatch = (shouldUpdateVersion) => {
|
|
1705
1779
|
console.log(`
|
|
1706
1780
|
\uD83D\uDCCB Starting patch release from main...
|
|
1707
1781
|
`);
|
|
1708
1782
|
git("checkout main");
|
|
1709
1783
|
git("pull");
|
|
1710
|
-
|
|
1784
|
+
if (shouldUpdateVersion)
|
|
1785
|
+
npmVersion("patch");
|
|
1711
1786
|
git("push --follow-tags");
|
|
1712
1787
|
git("checkout develop");
|
|
1713
1788
|
git("pull");
|
|
@@ -1718,8 +1793,8 @@ function releasePatch() {
|
|
|
1718
1793
|
console.log(`
|
|
1719
1794
|
✅ Patch release complete!
|
|
1720
1795
|
`);
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1796
|
+
};
|
|
1797
|
+
var releaseFromDevelop = (kind, shouldUpdateVersion) => {
|
|
1723
1798
|
console.log(`
|
|
1724
1799
|
\uD83D\uDCCB Starting ${kind} release from develop...
|
|
1725
1800
|
`);
|
|
@@ -1727,7 +1802,8 @@ function releaseFromDevelop(kind) {
|
|
|
1727
1802
|
if (current !== "develop")
|
|
1728
1803
|
throw new Error(`must be on 'develop', currently on '${current}'.`);
|
|
1729
1804
|
git("pull");
|
|
1730
|
-
|
|
1805
|
+
if (shouldUpdateVersion)
|
|
1806
|
+
npmVersion(kind);
|
|
1731
1807
|
git("push --follow-tags");
|
|
1732
1808
|
git("checkout main");
|
|
1733
1809
|
git("pull");
|
|
@@ -1735,21 +1811,22 @@ function releaseFromDevelop(kind) {
|
|
|
1735
1811
|
git("push");
|
|
1736
1812
|
git("checkout develop");
|
|
1737
1813
|
git("pull origin main");
|
|
1814
|
+
git("push");
|
|
1738
1815
|
assertSynced(["main", "develop"]);
|
|
1739
1816
|
console.log(`
|
|
1740
1817
|
✅ ${kind} release complete!
|
|
1741
1818
|
`);
|
|
1742
|
-
}
|
|
1819
|
+
};
|
|
1743
1820
|
|
|
1744
1821
|
// src/components/setup/setup.ts
|
|
1745
1822
|
import {
|
|
1746
1823
|
chmodSync,
|
|
1747
|
-
existsSync as
|
|
1824
|
+
existsSync as existsSync5,
|
|
1748
1825
|
mkdirSync,
|
|
1749
1826
|
readFileSync as readFileSync3,
|
|
1750
1827
|
writeFileSync as writeFileSync2
|
|
1751
1828
|
} from "node:fs";
|
|
1752
|
-
import
|
|
1829
|
+
import path4 from "node:path";
|
|
1753
1830
|
|
|
1754
1831
|
// src/components/setup/setup.constants.ts
|
|
1755
1832
|
var HOOKS_DIR = ".githooks";
|
|
@@ -1770,8 +1847,8 @@ var RUN_PREFIX = {
|
|
|
1770
1847
|
};
|
|
1771
1848
|
|
|
1772
1849
|
// src/components/setup/setup.utils.ts
|
|
1773
|
-
import { existsSync as
|
|
1774
|
-
import
|
|
1850
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1851
|
+
import path3 from "node:path";
|
|
1775
1852
|
function checkBlock(manager, script, label) {
|
|
1776
1853
|
return `echo "▶ ${label}"
|
|
1777
1854
|
${RUN_PREFIX[manager]} ${script}
|
|
@@ -1791,11 +1868,11 @@ set -e
|
|
|
1791
1868
|
${checks}`;
|
|
1792
1869
|
}
|
|
1793
1870
|
function detectPackageManager(projectPath) {
|
|
1794
|
-
const lockfile = LOCKFILES.find(([file]) =>
|
|
1871
|
+
const lockfile = LOCKFILES.find(([file]) => existsSync4(path3.join(projectPath, file)));
|
|
1795
1872
|
return lockfile?.[1] ?? "npm";
|
|
1796
1873
|
}
|
|
1797
1874
|
function isTypeScriptProject(projectPath, pkg) {
|
|
1798
|
-
if (
|
|
1875
|
+
if (existsSync4(path3.join(projectPath, "tsconfig.json")))
|
|
1799
1876
|
return true;
|
|
1800
1877
|
return Boolean(pkg.dependencies?.typescript || pkg.devDependencies?.typescript);
|
|
1801
1878
|
}
|
|
@@ -1818,14 +1895,14 @@ function setup(flags) {
|
|
|
1818
1895
|
}
|
|
1819
1896
|
function setupNpmHook({ force }) {
|
|
1820
1897
|
const root = process.cwd();
|
|
1821
|
-
if (!
|
|
1898
|
+
if (!existsSync5(path4.join(root, ".git"))) {
|
|
1822
1899
|
throw new Error("not a git repository root: run it in the folder that holds .git");
|
|
1823
1900
|
}
|
|
1824
|
-
const packagePath =
|
|
1825
|
-
if (!
|
|
1901
|
+
const packagePath = path4.join(root, "package.json");
|
|
1902
|
+
if (!existsSync5(packagePath))
|
|
1826
1903
|
throw new Error("no package.json in this folder");
|
|
1827
|
-
const hookPath =
|
|
1828
|
-
if (
|
|
1904
|
+
const hookPath = path4.join(root, HOOK_FILE);
|
|
1905
|
+
if (existsSync5(hookPath) && !force) {
|
|
1829
1906
|
throw new Error(`${HOOK_FILE} already exists. Pass --force to overwrite it.`);
|
|
1830
1907
|
}
|
|
1831
1908
|
const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
|
|
@@ -1836,7 +1913,7 @@ function setupNpmHook({ force }) {
|
|
|
1836
1913
|
console.log(`dry run: would write ${HOOK_FILE} (package manager: ${manager})`);
|
|
1837
1914
|
return;
|
|
1838
1915
|
}
|
|
1839
|
-
mkdirSync(
|
|
1916
|
+
mkdirSync(path4.join(root, HOOKS_DIR), { recursive: true });
|
|
1840
1917
|
writeFileSync2(hookPath, hook);
|
|
1841
1918
|
try {
|
|
1842
1919
|
chmodSync(hookPath, 493);
|
|
@@ -2108,19 +2185,32 @@ function parseStatus(output) {
|
|
|
2108
2185
|
}
|
|
2109
2186
|
return status;
|
|
2110
2187
|
}
|
|
2111
|
-
|
|
2188
|
+
var status = (flags) => {
|
|
2112
2189
|
if (flags.size > 0)
|
|
2113
2190
|
throw new Error("usage: gith status");
|
|
2114
2191
|
const info = parseStatus(captureArgs(["status", "--porcelain=v2", "--branch"]));
|
|
2115
2192
|
const against = info.upstream ? `${info.branch} -> ${info.upstream} ahead ${info.ahead}, behind ${info.behind}` : `${info.branch} (no upstream)`;
|
|
2116
|
-
|
|
2193
|
+
process.stdout.write(`${against}
|
|
2194
|
+
`);
|
|
2117
2195
|
const changed = info.staged + info.unstaged + info.untracked + info.conflicts;
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2196
|
+
process.stdout.write(changed === 0 ? `working tree clean
|
|
2197
|
+
` : `staged ${info.staged}, unstaged ${info.unstaged}, untracked ${info.untracked}, conflicts ${info.conflicts}
|
|
2198
|
+
`);
|
|
2199
|
+
for (const [heading, scope] of [
|
|
2200
|
+
["Local branches (* current, + other worktree)", "--list"],
|
|
2201
|
+
["Remote branches (last fetched)", "--remotes"]
|
|
2202
|
+
]) {
|
|
2203
|
+
const branches = captureArgs(["branch", "--no-color", scope]);
|
|
2204
|
+
process.stdout.write(`
|
|
2205
|
+
${heading}:
|
|
2206
|
+
`);
|
|
2207
|
+
process.stdout.write(branches.length === 0 ? ` (none)
|
|
2208
|
+
` : `${branches.split(`
|
|
2209
|
+
`).map((branch) => ` ${branch.trim()}`).join(`
|
|
2210
|
+
`)}
|
|
2211
|
+
`);
|
|
2121
2212
|
}
|
|
2122
|
-
|
|
2123
|
-
}
|
|
2213
|
+
};
|
|
2124
2214
|
|
|
2125
2215
|
// src/components/tree/tree.utils.ts
|
|
2126
2216
|
function flagsOf(worktree) {
|
|
@@ -2190,32 +2280,20 @@ function tree(flags) {
|
|
|
2190
2280
|
// src/components/version/version.ts
|
|
2191
2281
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2192
2282
|
|
|
2193
|
-
// src/components/version/version.constants.ts
|
|
2194
|
-
var MASCOT = [
|
|
2195
|
-
" o",
|
|
2196
|
-
" /",
|
|
2197
|
-
" o----o----o",
|
|
2198
|
-
" \\ | /",
|
|
2199
|
-
" \\ | /",
|
|
2200
|
-
" (o_o)",
|
|
2201
|
-
" /|_|\\",
|
|
2202
|
-
" d b"
|
|
2203
|
-
];
|
|
2204
|
-
|
|
2205
2283
|
// src/components/version/version.utils.ts
|
|
2206
|
-
import { existsSync as
|
|
2207
|
-
import
|
|
2284
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
2285
|
+
import path5 from "node:path";
|
|
2208
2286
|
import { fileURLToPath } from "node:url";
|
|
2209
2287
|
function packageJsonPath() {
|
|
2210
|
-
let dir =
|
|
2288
|
+
let dir = path5.dirname(fileURLToPath(import.meta.url));
|
|
2211
2289
|
for (;; ) {
|
|
2212
|
-
const candidate =
|
|
2213
|
-
if (
|
|
2290
|
+
const candidate = path5.join(dir, "package.json");
|
|
2291
|
+
if (existsSync6(candidate)) {
|
|
2214
2292
|
const pkg = JSON.parse(readFileSync4(candidate, "utf8"));
|
|
2215
2293
|
if (pkg.name === "@tertium/gith")
|
|
2216
2294
|
return candidate;
|
|
2217
2295
|
}
|
|
2218
|
-
const parent =
|
|
2296
|
+
const parent = path5.dirname(dir);
|
|
2219
2297
|
if (parent === dir)
|
|
2220
2298
|
throw new Error("gith package.json not found");
|
|
2221
2299
|
dir = parent;
|
|
@@ -2229,7 +2307,18 @@ function readVersion() {
|
|
|
2229
2307
|
}
|
|
2230
2308
|
function versionText() {
|
|
2231
2309
|
const version = readVersion();
|
|
2232
|
-
const side = [
|
|
2310
|
+
const side = [
|
|
2311
|
+
"",
|
|
2312
|
+
"",
|
|
2313
|
+
"",
|
|
2314
|
+
"",
|
|
2315
|
+
"",
|
|
2316
|
+
"",
|
|
2317
|
+
"",
|
|
2318
|
+
"",
|
|
2319
|
+
`gith v${version}`,
|
|
2320
|
+
"git happens."
|
|
2321
|
+
];
|
|
2233
2322
|
return MASCOT.map((line, i) => `${line.padEnd(16)}${side[i] ?? ""}`.trimEnd()).join(`
|
|
2234
2323
|
`);
|
|
2235
2324
|
}
|
|
@@ -2242,6 +2331,7 @@ Start and finish work
|
|
|
2242
2331
|
gith create -r <version> release/<version> from develop
|
|
2243
2332
|
gith create -h <version> hotfix/<version> from main
|
|
2244
2333
|
gith create --release | --hotfix next version from the latest tag
|
|
2334
|
+
gith create -r <version> --update-version --npm also commit npm version on the new branch; works with -h, --release, --hotfix
|
|
2245
2335
|
gith switch list local and origin branches, and switch to the one picked
|
|
2246
2336
|
gith switch -b <branch> switch to a branch, creating develop--<branch> from develop if missing (on a detached HEAD: <branch> from that commit)
|
|
2247
2337
|
gith switch -commit <sha> detach HEAD at a commit, to look at it (tracked files must be clean)
|
|
@@ -2255,7 +2345,7 @@ Start and finish work
|
|
|
2255
2345
|
gith merge -r <v> | -h <v> merge into release/<v> or hotfix/<v>
|
|
2256
2346
|
gith merge --current-release merge this branch into the newest release (local or origin)
|
|
2257
2347
|
gith merge --current-hotfix merge this branch into the newest hotfix (local or origin)
|
|
2258
|
-
gith release --patch | --minor | --major
|
|
2348
|
+
gith release --patch | --minor | --major sync release branches; add --update-version --npm to bump npm version
|
|
2259
2349
|
|
|
2260
2350
|
Fix history
|
|
2261
2351
|
gith undo undo the last commit, keep its changes staged (not if pushed)
|
|
@@ -2282,10 +2372,10 @@ Clean up branches (every removal shows what it will remove first)
|
|
|
2282
2372
|
gith rm alias of remove
|
|
2283
2373
|
|
|
2284
2374
|
Look at the repo
|
|
2285
|
-
gith status
|
|
2375
|
+
gith status working-tree summary, all local and remote-tracking branches
|
|
2286
2376
|
gith list --commits [--graph] [-l <n>] newest commits on this branch; --graph adds colored lanes, refs, author/date (default 5)
|
|
2287
2377
|
gith list --tags [-l <n>] tags with a local and origin column (newest 5 by default)
|
|
2288
|
-
gith list --stashes [-l <n>] stashes, newest first (default 5); --pockets shows them with
|
|
2378
|
+
gith list --stashes [-l <n>] stashes, newest first (default 5); --pockets shows them with the gith mascot
|
|
2289
2379
|
gith show ... same as gith list
|
|
2290
2380
|
gith tree [-l <n>] working trees: path, branch, HEAD (current marked *)
|
|
2291
2381
|
gith find|search [-commit <text>] [-file <pattern>] [-change <text>] [--regex] [-limit <n>]
|
|
@@ -2315,6 +2405,7 @@ Flags: every single-dash flag requires a value; double-dash flags never take one
|
|
|
2315
2405
|
-b -r -h -l -m -n -limit -message -name -branch -release -hotfix -commit -file -change -days take values
|
|
2316
2406
|
--patch --minor --major --local --remote --obsolete --detached --tags --commits --graph --stashes --pockets --branch --regex
|
|
2317
2407
|
--develop --main --current-release --current-hotfix --all --&push --all-branches --no-commit --force
|
|
2408
|
+
--update-version --npm release or create release/hotfix: opt in to npm version updates (both required)
|
|
2318
2409
|
--overall --current-year --current-month --current-week --last-year --last-month --last-week switches
|
|
2319
2410
|
--branch lists removable branches; --release / --hotfix choose a version or branch by command context
|
|
2320
2411
|
-limit caps listings and stat (or -l); -days sets the age for remove --obsolete`;
|
|
@@ -2418,11 +2509,11 @@ try {
|
|
|
2418
2509
|
break;
|
|
2419
2510
|
case "release": {
|
|
2420
2511
|
const flags = parseFlags(rest);
|
|
2421
|
-
assertAllowedFlags(flags, ["patch", "minor", "major"], "release");
|
|
2512
|
+
assertAllowedFlags(flags, ["patch", "minor", "major", "update-version", "npm"], "release");
|
|
2422
2513
|
const kind = onlyOne(flags, ["patch", "minor", "major"]);
|
|
2423
2514
|
if (!kind)
|
|
2424
|
-
throw new Error("usage: gith release --patch | --minor | --major");
|
|
2425
|
-
release(kind);
|
|
2515
|
+
throw new Error("usage: gith release --patch | --minor | --major [--update-version --npm]");
|
|
2516
|
+
release(kind, flags);
|
|
2426
2517
|
break;
|
|
2427
2518
|
}
|
|
2428
2519
|
case "--version":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tertium/gith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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": {
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"format:check": "biome format src",
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
21
21
|
"prepare": "git config core.hooksPath .githooks || true",
|
|
22
|
-
"release:patch": "node bin/gith.js release --patch",
|
|
23
|
-
"release:minor": "node bin/gith.js release --minor",
|
|
24
|
-
"release:major": "node bin/gith.js release --major",
|
|
22
|
+
"release:patch": "node bin/gith.js release --patch --update-version --npm",
|
|
23
|
+
"release:minor": "node bin/gith.js release --minor --update-version --npm",
|
|
24
|
+
"release:major": "node bin/gith.js release --major --update-version --npm",
|
|
25
25
|
"version": "bun run build && node bin/gith.js changelog --no-commit && git add -- CHANGELOG.md"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|