@tertium/gith 0.3.3 → 0.4.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 +35 -19
- package/bin/gith.js +339 -93
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,7 +13,9 @@ gith: git-only branch flow CLI (goal branches, release/hotfix containers, synced
|
|
|
13
13
|
d b
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
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).
|
|
16
|
+
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
|
+
|
|
18
|
+
See [Command design rules](docs/RULES.md) before changing or adding commands and flags.
|
|
17
19
|
|
|
18
20
|
> **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.
|
|
19
21
|
|
|
@@ -24,20 +26,21 @@ Git helper: the branch flow from [git-convention](https://github.com/Tertiumnon/
|
|
|
24
26
|
gith create -b login-redirect # work branch develop--login-redirect from develop; stays on the current branch
|
|
25
27
|
gith create -r 1.2.0 # release/1.2.0 from develop
|
|
26
28
|
gith create -h 1.2.1 # hotfix/1.2.1 from main
|
|
27
|
-
gith create --release # release
|
|
28
|
-
gith create --hotfix # hotfix
|
|
29
|
+
gith create --release # next release version from the latest tag
|
|
30
|
+
gith create --hotfix # next hotfix version from the latest tag
|
|
29
31
|
gith switch # list local and origin branches, and switch to the one you pick
|
|
30
32
|
gith switch -b login-redirect # switch to develop--login-redirect, creating it from develop if it is missing
|
|
31
33
|
gith switch -commit 3f2a1b9 # detach HEAD at that commit, to look at old code
|
|
32
34
|
gith switch --release # switch to the newest release/* (local or origin); -r 1.9.0 for one version
|
|
33
35
|
gith switch --hotfix # switch to the newest hotfix/* (local or origin); -h 1.2.1 for one version
|
|
34
36
|
gith update # merge origin/develop into this work branch
|
|
35
|
-
gith merge
|
|
37
|
+
gith merge --develop # this branch into develop
|
|
36
38
|
gith finish --current-release # release the newest release/*: version, changelog, merge to main, tag, merge main to develop, then ask before deleting the branch
|
|
37
39
|
gith finish --current-hotfix # the same for the newest hotfix/*
|
|
38
40
|
gith merge # pick the branch to merge, then the target from the list
|
|
39
41
|
gith merge --main # this branch into main
|
|
40
|
-
gith merge -r 1.2.0 # this branch into release/1.2.0
|
|
42
|
+
gith merge -r 1.2.0 # this branch into release/1.2.0
|
|
43
|
+
gith merge --release # this branch into the only release, or pick one
|
|
41
44
|
gith merge -h 1.2.1 # this branch into hotfix/1.2.1
|
|
42
45
|
gith merge --current-release # this branch into the newest release/* (local or origin)
|
|
43
46
|
gith merge --current-hotfix # this branch into the newest hotfix/* (local or origin)
|
|
@@ -47,7 +50,7 @@ gith release --major # npm version major, from develop into main
|
|
|
47
50
|
|
|
48
51
|
# Fix history
|
|
49
52
|
gith undo # undo the last commit, keep its changes staged (not if pushed)
|
|
50
|
-
gith save
|
|
53
|
+
gith save -m "msg" [--all] ["--&push"] # new commit without prompts; push only with "--&push"
|
|
51
54
|
gith squash -limit 3 [-m "msg"] # combine the last 3 commits into one (keeps the oldest message)
|
|
52
55
|
|
|
53
56
|
# Save and reset work
|
|
@@ -89,6 +92,8 @@ gith stat # fun facts about this branch: biggest commi
|
|
|
89
92
|
gith stat --last-week -limit 50 # the same for last week, looking at no more than 50 commits
|
|
90
93
|
|
|
91
94
|
# Remote and health
|
|
95
|
+
gith push [--all-branches] # push to origin with tracking and ahead/behind counts; never force
|
|
96
|
+
gith pull [--all-branches] # fast-forward from origin; stop on divergence
|
|
92
97
|
gith sync # fetch all remotes and tags, prune stale refs, then sync this branch with origin both ways
|
|
93
98
|
gith sync --develop | --main # the same for develop or main
|
|
94
99
|
gith sync develop:develop # two-way sync of one branch with origin
|
|
@@ -97,6 +102,10 @@ gith doctor # check the repo is ready for the branch flo
|
|
|
97
102
|
gith changelog --dry-run # preview the CHANGELOG.md entry for package.json's version
|
|
98
103
|
gith changelog # write it as its own commit (docs(changelog): <version>); --no-commit only writes
|
|
99
104
|
|
|
105
|
+
# Set up
|
|
106
|
+
gith setup -n npm # write .githooks/pre-commit (lint, typecheck, test) and set core.hooksPath
|
|
107
|
+
gith setup -n npm --force # overwrite an existing hook
|
|
108
|
+
|
|
100
109
|
# Help
|
|
101
110
|
gith help # full usage
|
|
102
111
|
gith --version # the version, with the mascot
|
|
@@ -107,34 +116,39 @@ gith list --tags --verbose
|
|
|
107
116
|
|
|
108
117
|
## Flags
|
|
109
118
|
|
|
110
|
-
One rule applies to every command: **a single dash always takes a value, and a double dash never does.** So `-limit 10` is right,
|
|
119
|
+
One rule applies to every command: **a single dash always takes a value, and a double dash never does.** So `-limit 10` is right, while `--limit 10`, `-remote`, `-r` and `-h` without values are errors. Use `--release` or `--hotfix` for automatic selection.
|
|
111
120
|
|
|
112
121
|
| Flag | Kind | Used by |
|
|
113
122
|
| --- | --- | --- |
|
|
114
123
|
| `-b <name>`, `-branch <name>` | value | `create`, `switch`, `remove` |
|
|
115
124
|
| `--branch` | alone: open the picker | `remove` |
|
|
116
|
-
| `-r <version>`, `-release <version>` | value | `create`, `merge` |
|
|
117
|
-
|
|
|
118
|
-
| `--release` | alone: version from the latest tag | `create` |
|
|
125
|
+
| `-r <version>`, `-release <version>` | value | `create`, `switch`, `merge` |
|
|
126
|
+
| `--release` | automatic version in `create`; newest container in `switch`; only container or picker in `merge` | `create`, `switch`, `merge` |
|
|
119
127
|
| `--main` | alone: merge into main | `merge` |
|
|
120
128
|
| `--current-release`, `--current-hotfix` | switch: merge into the newest release or hotfix | `merge` |
|
|
121
|
-
| `-h <version>`, `-hotfix <version>` | value | `create`, `merge` |
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
| `-
|
|
125
|
-
|
|
|
129
|
+
| `-h <version>`, `-hotfix <version>` | value | `create`, `switch`, `merge` |
|
|
130
|
+
| `--hotfix` | automatic version in `create`; newest container in `switch`; only container or picker in `merge` | `create`, `switch`, `merge` |
|
|
131
|
+
| `-limit <n>` | value | `squash`: commits to combine; listings and `stat`: rows or commits to show |
|
|
132
|
+
| `-m <msg>`, `-message <msg>` | value | `save` (required), `squash` |
|
|
133
|
+
| `--all` | switch | `save`: stage all changes |
|
|
134
|
+
| `"--&push"` | switch | `save`: push after committing |
|
|
135
|
+
| `--all-branches` | switch | `push`, `pull`: all local branches |
|
|
126
136
|
| `-n <name>`, `-name <name>` | value | `stash` |
|
|
137
|
+
| `-commit <sha>` | value | `switch`: detach at a commit |
|
|
138
|
+
| `-file <pattern>`, `-change <text>` | value | `find` |
|
|
127
139
|
| `-l <n>`, `-limit <n>` | value, positive whole number | every listing: `list`, `tree`, the picker |
|
|
128
140
|
| `--patch`, `--minor`, `--major` | switch | `release` |
|
|
129
141
|
| `--overall`, `--current-year`, `--current-month`, `--current-week`, `--last-year`, `--last-month`, `--last-week` | switch, one at a time | `stat` |
|
|
130
142
|
| `--local`, `--remote` | switch | `remove` (both sides when neither is given) |
|
|
131
143
|
| `--obsolete`, `--detached` | switch | `remove` |
|
|
144
|
+
| `--force` | switch | `setup`: replace an existing hook |
|
|
145
|
+
| `--no-commit` | switch | `changelog`: update the file without committing |
|
|
132
146
|
| `-days <n>` | value | `remove --obsolete` |
|
|
133
147
|
| `--tags`, `--commits`, `--graph`, `--stashes`, `--pockets` | switch | `list` (also `show`) |
|
|
134
148
|
| `--verbose` | switch | every command |
|
|
135
149
|
| `--dry-run` | switch | every command |
|
|
136
150
|
|
|
137
|
-
Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `list --tags` and `--commits`) accept only one of them. `-limit` caps listings and `stat`, and on `squash` it sets how many commits to combine. `-message`
|
|
151
|
+
Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `list --tags` and `--commits`) accept only one of them. `-limit` caps listings and `stat`, and on `squash` it sets how many commits to combine. `-message` applies to `save` and `squash`.
|
|
138
152
|
|
|
139
153
|
`--verbose` and `--dry-run` can go anywhere on the line:
|
|
140
154
|
- `--dry-run` prints each git command that would change something, and runs none of them. Read-only checks still run, so a refusal (for example, "HEAD is already on origin") appears in a dry run too. `clean` and `reset` also list the files they would remove or discard.
|
|
@@ -149,7 +163,8 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
149
163
|
- **`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.
|
|
150
164
|
- **`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.
|
|
151
165
|
- **`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.
|
|
152
|
-
- **`
|
|
166
|
+
- **`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.
|
|
167
|
+
- **`save`** requires a nonempty `-m` and always creates a new commit, without prompts or amending. It stages a single unstaged file automatically; use `--all` to stage several, or stage your selection with `git add`. With nothing staged it fails. It saves locally unless `"--&push"` is supplied, which runs `gith push` after committing. Quote the flag because shells interpret `&`. A failed push leaves the new commit saved locally; retry with `gith push`.
|
|
153
168
|
- **`stash`** saves changes with `git stash push --include-untracked`. `-n` names the stash. It refuses when there is nothing to save.
|
|
154
169
|
- **`find`** (also `search`) lists the newest commits on this branch that match every filter given:
|
|
155
170
|
- `-commit <text>` matches the commit message, ignoring case, and the text is literal.
|
|
@@ -166,12 +181,13 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
166
181
|
- **`remove --detached`** fetches and prunes origin, then lists local branches that have no origin copy and lets you pick from them, the same as `remove --branch --local` but for branches with no origin copy only.
|
|
167
182
|
- **Every removal shows what it will remove** before deleting anything, including the direct `remove -b` form. The picker and the prompt both show it first.
|
|
168
183
|
- **`open`** opens origin in the browser, turning an `ssh` or `https` remote into a web address. GitHub and GitLab style addresses work.
|
|
184
|
+
- **`setup -n npm`** writes `.githooks/pre-commit` for an npm project and sets `core.hooksPath` to `.githooks`, so every clone runs the same checks. The hook runs `lint`, `typecheck` and `test` (the scripts are picked up from package.json; TypeScript projects always run `lint` and `typecheck`, and `test` only when it exists). It uses the package manager from the lockfile (`bun`, `pnpm`, `yarn`, or `npm`). It adds a `prepare` script so installing the project turns the hooks on again. It refuses to overwrite an existing hook unless `--force` is given, and it must run in the folder that holds `.git`. Only `npm` is supported for now.
|
|
169
185
|
- **`doctor`** checks git, a repository, origin, its reachability, `main` and `develop` on origin, and a clean working tree. It exits 1 if a required check fails, and only warns about the tree.
|
|
170
186
|
- **`sync`** runs `git fetch --all --tags --prune`, then syncs the current branch with origin in both directions. `sync --develop` and `sync --main` do the same for that branch, even when another branch is checked out.
|
|
171
187
|
- **`sync <branch>:<branch>`** (for example `sync develop:develop`) is the same as `sync --develop` for any branch name. If origin is ahead, it fetches the branch like `git fetch origin develop:develop`; if local is ahead, it pushes it; if it is the same, it says so. A checked-out branch is fast-forwarded with `merge --ff-only`. Diverged branches stop with an error, and nothing is forced. A branch missing on one side is created there. `sync develop` alone works too.
|
|
172
188
|
- **`tree`** lists the working trees of the repository (`git worktree list`): each path, its branch or `(detached)`, and its short HEAD. The current one is marked `*`, and bare, locked or prunable trees are noted. `-limit <n>` caps the rows; all are shown by default.
|
|
173
189
|
- **`list --tags`** shows every tag once, with yes/no columns for local and origin, so you can see which tags are unpushed or missing locally. Version tags are listed newest first, and only the first 5 are shown by default. `-limit <n>` (or `-l <n>`) changes that and adds a note when rows are left out.
|
|
174
|
-
- **`list --commits`** shows the newest commits on the current branch, one per line: short hash, date, author and subject. With `--graph` it draws
|
|
190
|
+
- **`list --commits`** shows the newest commits on the current branch, one per line: short hash, date, author and subject. With `--graph` it draws a framed branch graph with colored topology lanes, highlighted hashes and branch/tag labels, and author/date rows. Colors are enabled in terminals and disabled for pipes, `NO_COLOR`, or `TERM=dumb`. Git preserves merge topology; history remains scoped to the current branch. It shows 5 by default; `-l <n>` (or `-limit <n>`) changes that and must be a positive whole number. `-limit` only applies to listings. `gith list` with no flag prints usage.
|
|
175
191
|
- **`clean`** runs `git clean -fd`. It permanently removes untracked files and directories, but keeps ignored files.
|
|
176
192
|
- **`reset`** runs `git reset --hard` (discards staged and unstaged edits on this branch), then `git clean -fd`. It cannot be undone, so commit or stash first. It does not move the branch to origin.
|
|
177
193
|
|
|
@@ -192,7 +208,7 @@ Containers (`develop`, `main`, `release/*`, `hotfix/*`) can't be merged from, an
|
|
|
192
208
|
Run as `gith release --patch`, `--minor` or `--major`.
|
|
193
209
|
|
|
194
210
|
- **`patch`** runs from `main`: bumps the version, 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.
|
|
195
|
-
- **`minor` and `major`** run from `develop`: bump the version, push with tags, merge `develop` into `main`, then bring `main` back to `develop`.
|
|
211
|
+
- **`minor` and `major`** run from `develop`: bump the version with its changelog entry, push with tags, merge `develop` into `main`, then bring `main` back to `develop`.
|
|
196
212
|
|
|
197
213
|
Both need a clean working tree and a `package.json` with a version, because they call `npm version`.
|
|
198
214
|
|
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", "auto"],
|
|
10
|
+
["hotfix", "auto"],
|
|
11
11
|
["limit", "value"],
|
|
12
12
|
["message", "value"],
|
|
13
13
|
["name", "value"],
|
|
@@ -33,7 +33,10 @@ var SPECS = new Map([
|
|
|
33
33
|
["pockets", "switch"],
|
|
34
34
|
["detached", "switch"],
|
|
35
35
|
["all", "switch"],
|
|
36
|
+
["&push", "switch"],
|
|
37
|
+
["all-branches", "switch"],
|
|
36
38
|
["no-commit", "switch"],
|
|
39
|
+
["force", "switch"],
|
|
37
40
|
["overall", "switch"],
|
|
38
41
|
["current-year", "switch"],
|
|
39
42
|
["current-month", "switch"],
|
|
@@ -57,8 +60,13 @@ var SHORT = {
|
|
|
57
60
|
};
|
|
58
61
|
|
|
59
62
|
// src/components/args/args.utils.ts
|
|
63
|
+
var assertAllowedFlags = (flags, allowed, command) => {
|
|
64
|
+
const extra = [...flags.keys()].find((key) => !allowed.includes(key));
|
|
65
|
+
if (extra !== undefined)
|
|
66
|
+
throw new Error(`${spelling(extra)} is not supported by gith ${command}.`);
|
|
67
|
+
};
|
|
60
68
|
function resolveFlag(token) {
|
|
61
|
-
const match = /^(--?)([a-z][a-z-]
|
|
69
|
+
const match = /^(--?)([a-z][a-z-]*|&push)$/.exec(token);
|
|
62
70
|
if (!match)
|
|
63
71
|
return;
|
|
64
72
|
const word = match[2] ?? "";
|
|
@@ -93,8 +101,6 @@ function parseFlags(argv) {
|
|
|
93
101
|
if (hasNext) {
|
|
94
102
|
flags.set(name, next);
|
|
95
103
|
i++;
|
|
96
|
-
} else if (kind === "optional") {
|
|
97
|
-
flags.set(name, "");
|
|
98
104
|
} else {
|
|
99
105
|
throw new Error(`${token} needs a value.`);
|
|
100
106
|
}
|
|
@@ -166,7 +172,7 @@ function capture(args) {
|
|
|
166
172
|
return run(args.split(" "));
|
|
167
173
|
}
|
|
168
174
|
function hasOnOrigin(branch) {
|
|
169
|
-
return
|
|
175
|
+
return captureArgs(["ls-remote", "--heads", "origin", `refs/heads/${branch}`]) !== "";
|
|
170
176
|
}
|
|
171
177
|
function isAncestor(ancestor, descendant) {
|
|
172
178
|
try {
|
|
@@ -488,20 +494,6 @@ async function confirm(question) {
|
|
|
488
494
|
rl.close();
|
|
489
495
|
}
|
|
490
496
|
}
|
|
491
|
-
async function ask(question) {
|
|
492
|
-
if (isDryRun()) {
|
|
493
|
-
console.log(`dry run: would ask: ${question}`);
|
|
494
|
-
return "<message>";
|
|
495
|
-
}
|
|
496
|
-
if (!process.stdin.isTTY)
|
|
497
|
-
throw new Error("needs a terminal to ask; pass -m <message>.");
|
|
498
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
499
|
-
try {
|
|
500
|
-
return (await rl.question(question)).trim();
|
|
501
|
-
} finally {
|
|
502
|
-
rl.close();
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
497
|
async function select(question, options, multi) {
|
|
506
498
|
if (!process.stdin.isTTY)
|
|
507
499
|
throw new Error("this needs a terminal to pick.");
|
|
@@ -660,9 +652,10 @@ function open(kind, version) {
|
|
|
660
652
|
|
|
661
653
|
// src/components/flow/flow.ts
|
|
662
654
|
function create(flags) {
|
|
655
|
+
assertAllowedFlags(flags, ["branch", "release", "hotfix"], "create");
|
|
663
656
|
const kind = onlyOne(flags, ["branch", "release", "hotfix"]);
|
|
664
657
|
if (!kind)
|
|
665
|
-
throw new Error("usage: gith create -b <branch> | -r
|
|
658
|
+
throw new Error("usage: gith create -b <branch> | -r <version> | -h <version> | --release | --hotfix");
|
|
666
659
|
const value = flags.get(kind) ?? "";
|
|
667
660
|
if (kind === "branch") {
|
|
668
661
|
if (!value)
|
|
@@ -673,6 +666,14 @@ function create(flags) {
|
|
|
673
666
|
open(kind, value || nextContainerVersion(kind));
|
|
674
667
|
}
|
|
675
668
|
async function merge(flags) {
|
|
669
|
+
assertAllowedFlags(flags, [
|
|
670
|
+
"develop",
|
|
671
|
+
"main",
|
|
672
|
+
"release",
|
|
673
|
+
"hotfix",
|
|
674
|
+
"current-release",
|
|
675
|
+
"current-hotfix"
|
|
676
|
+
], "merge");
|
|
676
677
|
if (flags.size === 0)
|
|
677
678
|
return mergePick();
|
|
678
679
|
const kind = onlyOne(flags, [
|
|
@@ -892,15 +893,109 @@ function assertNotPushed() {
|
|
|
892
893
|
throw new Error("HEAD is already on origin; changing it would need a force-push.");
|
|
893
894
|
}
|
|
894
895
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
if (
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
896
|
+
|
|
897
|
+
// src/components/remote/remote.utils.ts
|
|
898
|
+
var getBranches = (flags, command) => {
|
|
899
|
+
if ([...flags.keys()].some((key) => key !== "all-branches"))
|
|
900
|
+
throw new Error(`usage: gith ${command} [--all-branches]`);
|
|
901
|
+
if (!flags.has("all-branches"))
|
|
902
|
+
return [currentBranch()];
|
|
903
|
+
const branches = captureArgs([
|
|
904
|
+
"for-each-ref",
|
|
905
|
+
"--format=%(refname:strip=2)",
|
|
906
|
+
"refs/heads/"
|
|
907
|
+
]);
|
|
908
|
+
if (branches === "")
|
|
909
|
+
throw new Error("No local branches; create a branch and commit before syncing.");
|
|
910
|
+
return branches.split(`
|
|
911
|
+
`);
|
|
912
|
+
};
|
|
913
|
+
var compareOrigin = (branch) => {
|
|
914
|
+
gitArgs(["fetch", "--no-tags", "origin", `refs/heads/${branch}`]);
|
|
915
|
+
const counts = captureArgs([
|
|
916
|
+
"rev-list",
|
|
917
|
+
"--left-right",
|
|
918
|
+
"--count",
|
|
919
|
+
`refs/heads/${branch}...FETCH_HEAD`
|
|
920
|
+
]);
|
|
921
|
+
if (!/^\d+\s+\d+$/.test(counts))
|
|
922
|
+
throw new Error("Cannot compare commits with origin; check the repository with git fsck.");
|
|
923
|
+
const parts = counts.split(/\s+/);
|
|
924
|
+
const ahead = Number(parts[0]);
|
|
925
|
+
const behind = Number(parts[1]);
|
|
926
|
+
process.stdout.write(`${branch} -> origin/${branch}: ahead ${ahead}, behind ${behind}.
|
|
927
|
+
`);
|
|
928
|
+
return { ahead, behind };
|
|
929
|
+
};
|
|
930
|
+
var updateBranch = (branch, current) => {
|
|
931
|
+
if (branch === current) {
|
|
932
|
+
gitArgs(["fetch", "--no-tags", "origin", `refs/heads/${branch}`]);
|
|
933
|
+
gitArgs(["merge", "--ff-only", "FETCH_HEAD"]);
|
|
934
|
+
} else {
|
|
935
|
+
gitArgs([
|
|
936
|
+
"fetch",
|
|
937
|
+
"--no-tags",
|
|
938
|
+
"origin",
|
|
939
|
+
`refs/heads/${branch}:refs/heads/${branch}`
|
|
940
|
+
]);
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
// src/components/remote/remote.ts
|
|
945
|
+
var push = (flags) => {
|
|
946
|
+
const branches = getBranches(flags, "push");
|
|
947
|
+
for (const branch of branches) {
|
|
948
|
+
if (isDryRun())
|
|
949
|
+
continue;
|
|
950
|
+
if (!hasOnOrigin(branch)) {
|
|
951
|
+
process.stdout.write(`Publishing ${branch} on origin and setting upstream tracking.
|
|
952
|
+
`);
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
const { ahead, behind } = compareOrigin(branch);
|
|
956
|
+
if (behind > 0)
|
|
957
|
+
throw new Error(ahead > 0 ? `${branch} has diverged; merge or rebase origin's changes, then run gith push. Nothing was pushed.` : `${branch} is behind origin; run gith pull before gith push. Nothing was pushed.`);
|
|
958
|
+
process.stdout.write(`Pushing ${ahead} commit(s) from ${branch}; ensuring upstream tracking.
|
|
959
|
+
`);
|
|
960
|
+
}
|
|
961
|
+
gitArgs([
|
|
962
|
+
"push",
|
|
963
|
+
...flags.has("all-branches") ? ["--atomic"] : [],
|
|
964
|
+
"-u",
|
|
965
|
+
"origin",
|
|
966
|
+
...branches.map((branch) => `refs/heads/${branch}:refs/heads/${branch}`)
|
|
967
|
+
]);
|
|
968
|
+
if (!isDryRun())
|
|
969
|
+
process.stdout.write(`Push complete: ${branches.length} branch(es) synced with origin.
|
|
970
|
+
`);
|
|
971
|
+
};
|
|
972
|
+
var pull = (flags) => {
|
|
973
|
+
const branches = getBranches(flags, "pull");
|
|
974
|
+
const current = currentBranch();
|
|
975
|
+
assertCleanTree();
|
|
976
|
+
const updates = [];
|
|
977
|
+
for (const branch of branches) {
|
|
978
|
+
if (isDryRun())
|
|
979
|
+
continue;
|
|
980
|
+
if (!hasOnOrigin(branch))
|
|
981
|
+
throw new Error(`origin/${branch} does not exist; run gith push to publish this branch first.`);
|
|
982
|
+
const { ahead, behind } = compareOrigin(branch);
|
|
983
|
+
if (ahead > 0 && behind > 0)
|
|
984
|
+
throw new Error(`${branch} has diverged; merge or rebase origin's changes explicitly. No commits were changed.`);
|
|
985
|
+
if (behind > 0)
|
|
986
|
+
updates.push({ branch, behind });
|
|
987
|
+
else
|
|
988
|
+
process.stdout.write(ahead > 0 ? `${branch}: no incoming commits; run gith push to publish ${ahead} local commit(s).
|
|
989
|
+
` : `${branch}: already up to date.
|
|
990
|
+
`);
|
|
991
|
+
}
|
|
992
|
+
for (const { branch, behind } of isDryRun() ? branches.map((branch) => ({ branch, behind: 0 })) : updates) {
|
|
993
|
+
updateBranch(branch, current);
|
|
994
|
+
if (!isDryRun())
|
|
995
|
+
process.stdout.write(`Pulled ${behind} commit(s) into ${branch}.
|
|
996
|
+
`);
|
|
997
|
+
}
|
|
998
|
+
};
|
|
904
999
|
|
|
905
1000
|
// src/components/history/history.ts
|
|
906
1001
|
function update(flags) {
|
|
@@ -926,43 +1021,33 @@ function undo(flags) {
|
|
|
926
1021
|
assertNotPushed();
|
|
927
1022
|
gitArgs(["reset", "--soft", "HEAD~1"]);
|
|
928
1023
|
}
|
|
929
|
-
|
|
1024
|
+
var save = (flags) => {
|
|
930
1025
|
const message = flags.get("message");
|
|
931
|
-
const extra = [...flags.keys()].filter((key) => key !== "message" && key !== "all");
|
|
932
|
-
if (extra.length > 0)
|
|
933
|
-
throw new Error(
|
|
934
|
-
|
|
1026
|
+
const extra = [...flags.keys()].filter((key) => key !== "message" && key !== "all" && key !== "&push");
|
|
1027
|
+
if (extra.length > 0 || message === undefined || message.trim() === "")
|
|
1028
|
+
throw new Error('usage: gith save -m <message> [--all] ["--&push"]');
|
|
1029
|
+
currentBranch();
|
|
935
1030
|
const unstaged = captureArgs(["add", "-A", "--dry-run"]).split(`
|
|
936
1031
|
`).filter(Boolean);
|
|
937
|
-
const stageAll = flags.has("all") || unstaged.length === 1
|
|
1032
|
+
const stageAll = flags.has("all") || unstaged.length === 1;
|
|
938
1033
|
if (stageAll && unstaged.length > 0) {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1034
|
+
process.stdout.write(`Staging ${unstaged.length} unstaged change(s):
|
|
1035
|
+
${unstaged.join(`
|
|
1036
|
+
`)}
|
|
1037
|
+
`);
|
|
942
1038
|
}
|
|
943
1039
|
if (stageAll)
|
|
944
1040
|
gitArgs(["add", "-A"]);
|
|
945
|
-
if (captureArgs(["diff", "--cached", "--name-only"]) === "") {
|
|
946
|
-
throw new Error("nothing staged; stage the changes
|
|
947
|
-
}
|
|
948
|
-
const previous = captureArgs(["rev-parse", "HEAD"]);
|
|
949
|
-
const amend = hasOwnCommit() && await confirm("Amend the previous commit? [Y/n] ");
|
|
950
|
-
if (amend) {
|
|
951
|
-
gitArgs(message ? ["commit", "--amend", "-m", message] : ["commit", "--amend", "--no-edit"]);
|
|
952
|
-
} else {
|
|
953
|
-
gitArgs(["commit", "-m", message ?? await askMessage()]);
|
|
1041
|
+
if (captureArgs(["diff", "--cached", "--name-only"]) === "" && !(isDryRun() && stageAll && unstaged.length > 0)) {
|
|
1042
|
+
throw new Error("nothing staged; stage the changes with git add, or use gith save -m <message> --all.");
|
|
954
1043
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
gitArgs(["push", "--force-with-lease", "origin", branch]);
|
|
963
|
-
else
|
|
964
|
-
gitArgs(["push", "-u", "origin", branch]);
|
|
965
|
-
}
|
|
1044
|
+
gitArgs(["commit", "-m", message]);
|
|
1045
|
+
process.stdout.write(isDryRun() ? `Would save locally.
|
|
1046
|
+
` : `Saved locally.
|
|
1047
|
+
`);
|
|
1048
|
+
if (flags.has("&push"))
|
|
1049
|
+
push(new Map);
|
|
1050
|
+
};
|
|
966
1051
|
async function squash(flags) {
|
|
967
1052
|
const count = parseLimit(flags);
|
|
968
1053
|
const message = flags.get("message");
|
|
@@ -1004,17 +1089,19 @@ async function squash(flags) {
|
|
|
1004
1089
|
else
|
|
1005
1090
|
gitArgs(["commit", "-C", oldest]);
|
|
1006
1091
|
}
|
|
1007
|
-
function hasOwnCommit() {
|
|
1008
|
-
for (const base of ["origin/develop", "origin/main"]) {
|
|
1009
|
-
try {
|
|
1010
|
-
return captureArgs(["rev-list", "--count", `${base}..HEAD`]) !== "0";
|
|
1011
|
-
} catch {}
|
|
1012
|
-
}
|
|
1013
|
-
return true;
|
|
1014
|
-
}
|
|
1015
1092
|
|
|
1016
1093
|
// src/components/list/list.constants.ts
|
|
1017
1094
|
var DEFAULT_LIMIT2 = 5;
|
|
1095
|
+
var GRAPH_ARGS = [
|
|
1096
|
+
"-c",
|
|
1097
|
+
"log.graphColors=cyan,magenta,blue,yellow,green,red",
|
|
1098
|
+
"log",
|
|
1099
|
+
"--graph",
|
|
1100
|
+
"--topo-order",
|
|
1101
|
+
"--decorate=short",
|
|
1102
|
+
"--date=format:%Y-%m-%d %H:%M",
|
|
1103
|
+
"--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
|
+
];
|
|
1018
1105
|
var GORLUM = [
|
|
1019
1106
|
' .-"""-.',
|
|
1020
1107
|
" / o o \\",
|
|
@@ -1027,14 +1114,35 @@ var GORLUM = [
|
|
|
1027
1114
|
function yesNo(value) {
|
|
1028
1115
|
return value ? "yes" : "no";
|
|
1029
1116
|
}
|
|
1030
|
-
|
|
1117
|
+
var commits = (limit, graph) => {
|
|
1031
1118
|
if (!hasHead()) {
|
|
1032
|
-
|
|
1119
|
+
process.stdout.write(`No commits.
|
|
1120
|
+
`);
|
|
1033
1121
|
return;
|
|
1034
1122
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1123
|
+
if (!graph) {
|
|
1124
|
+
process.stdout.write(`${captureArgs(["log", `-n${limit}`, "--date=short", "--format=%h %ad %an %s"])}
|
|
1125
|
+
`);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
const { NO_COLOR: noColor, TERM: terminal } = process.env;
|
|
1129
|
+
const color = process.stdout.isTTY && noColor === undefined && terminal !== "dumb";
|
|
1130
|
+
const branch = captureArgs(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
1131
|
+
process.stdout.write(`
|
|
1132
|
+
╭─ COMMIT GRAPH ─────────────────────────────────────────
|
|
1133
|
+
│ ${branch} · branch history · limit ${limit}
|
|
1134
|
+
│
|
|
1135
|
+
`);
|
|
1136
|
+
process.stdout.write(`${captureArgs([
|
|
1137
|
+
...GRAPH_ARGS,
|
|
1138
|
+
`-n${limit}`,
|
|
1139
|
+
color ? "--color=always" : "--color=never"
|
|
1140
|
+
])}
|
|
1141
|
+
`);
|
|
1142
|
+
process.stdout.write(`╰─ * commit · branches & tags in parentheses · -l <n> shows more
|
|
1143
|
+
|
|
1144
|
+
`);
|
|
1145
|
+
};
|
|
1038
1146
|
function hasHead() {
|
|
1039
1147
|
try {
|
|
1040
1148
|
captureArgs(["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
@@ -1064,6 +1172,7 @@ function tagPairs(local, origin) {
|
|
|
1064
1172
|
});
|
|
1065
1173
|
}
|
|
1066
1174
|
function list(flags) {
|
|
1175
|
+
assertAllowedFlags(flags, ["tags", "commits", "stashes", "pockets", "graph", "limit"], "list");
|
|
1067
1176
|
const kind = onlyOne(flags, ["tags", "commits", "stashes", "pockets"]);
|
|
1068
1177
|
if (!kind)
|
|
1069
1178
|
throw new Error("usage: gith list --tags | --commits [--graph] | --stashes | --pockets [-limit <n>]");
|
|
@@ -1196,6 +1305,7 @@ async function switchBranch(flags) {
|
|
|
1196
1305
|
switchToCommit(commit);
|
|
1197
1306
|
return;
|
|
1198
1307
|
}
|
|
1308
|
+
assertAllowedFlags(flags, ["branch"], "switch");
|
|
1199
1309
|
const branch = requireBranch(flags, "switch");
|
|
1200
1310
|
const from = isDetached() ? "HEAD" : undefined;
|
|
1201
1311
|
const exists = (name) => hasLocal(name) || hasOnOrigin(name);
|
|
@@ -1262,6 +1372,16 @@ function detachedOptions(times, current) {
|
|
|
1262
1372
|
return options.sort((x, y) => x.name.localeCompare(y.name));
|
|
1263
1373
|
}
|
|
1264
1374
|
async function remove(flags) {
|
|
1375
|
+
assertAllowedFlags(flags, [
|
|
1376
|
+
"branch",
|
|
1377
|
+
"local",
|
|
1378
|
+
"remote",
|
|
1379
|
+
"obsolete",
|
|
1380
|
+
"detached",
|
|
1381
|
+
"days",
|
|
1382
|
+
"all",
|
|
1383
|
+
"limit"
|
|
1384
|
+
], "remove");
|
|
1265
1385
|
const name = flags.get("branch") ?? "";
|
|
1266
1386
|
const side = onlyOne(flags, ["local", "remote"]);
|
|
1267
1387
|
const scope = side ?? "pair";
|
|
@@ -1621,6 +1741,115 @@ function releaseFromDevelop(kind) {
|
|
|
1621
1741
|
`);
|
|
1622
1742
|
}
|
|
1623
1743
|
|
|
1744
|
+
// src/components/setup/setup.ts
|
|
1745
|
+
import {
|
|
1746
|
+
chmodSync,
|
|
1747
|
+
existsSync as existsSync4,
|
|
1748
|
+
mkdirSync,
|
|
1749
|
+
readFileSync as readFileSync3,
|
|
1750
|
+
writeFileSync as writeFileSync2
|
|
1751
|
+
} from "node:fs";
|
|
1752
|
+
import path3 from "node:path";
|
|
1753
|
+
|
|
1754
|
+
// src/components/setup/setup.constants.ts
|
|
1755
|
+
var HOOKS_DIR = ".githooks";
|
|
1756
|
+
var HOOK_FILE = `${HOOKS_DIR}/pre-commit`;
|
|
1757
|
+
var HOOKS_PATH_COMMAND = `git config core.hooksPath ${HOOKS_DIR}`;
|
|
1758
|
+
var LOCKFILES = [
|
|
1759
|
+
["bun.lock", "bun"],
|
|
1760
|
+
["bun.lockb", "bun"],
|
|
1761
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
1762
|
+
["yarn.lock", "yarn"],
|
|
1763
|
+
["package-lock.json", "npm"]
|
|
1764
|
+
];
|
|
1765
|
+
var RUN_PREFIX = {
|
|
1766
|
+
bun: "bun run",
|
|
1767
|
+
pnpm: "pnpm run",
|
|
1768
|
+
yarn: "yarn",
|
|
1769
|
+
npm: "npm run"
|
|
1770
|
+
};
|
|
1771
|
+
|
|
1772
|
+
// src/components/setup/setup.utils.ts
|
|
1773
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
1774
|
+
import path2 from "node:path";
|
|
1775
|
+
function checkBlock(manager, script, label) {
|
|
1776
|
+
return `echo "▶ ${label}"
|
|
1777
|
+
${RUN_PREFIX[manager]} ${script}
|
|
1778
|
+
|
|
1779
|
+
`;
|
|
1780
|
+
}
|
|
1781
|
+
function buildPreCommitHook(pkg, manager, isTypeScript) {
|
|
1782
|
+
const checks = [
|
|
1783
|
+
isTypeScript || pkg.scripts?.lint ? checkBlock(manager, "lint", "Lint") : "",
|
|
1784
|
+
isTypeScript || pkg.scripts?.typecheck ? checkBlock(manager, "typecheck", "Typecheck") : "",
|
|
1785
|
+
pkg.scripts?.test ? checkBlock(manager, "test", "Test") : ""
|
|
1786
|
+
].join("");
|
|
1787
|
+
return `#!/usr/bin/env sh
|
|
1788
|
+
# Generated by gith setup -n npm - runs on every commit via core.hooksPath
|
|
1789
|
+
set -e
|
|
1790
|
+
|
|
1791
|
+
${checks}`;
|
|
1792
|
+
}
|
|
1793
|
+
function detectPackageManager(projectPath) {
|
|
1794
|
+
const lockfile = LOCKFILES.find(([file]) => existsSync3(path2.join(projectPath, file)));
|
|
1795
|
+
return lockfile?.[1] ?? "npm";
|
|
1796
|
+
}
|
|
1797
|
+
function isTypeScriptProject(projectPath, pkg) {
|
|
1798
|
+
if (existsSync3(path2.join(projectPath, "tsconfig.json")))
|
|
1799
|
+
return true;
|
|
1800
|
+
return Boolean(pkg.dependencies?.typescript || pkg.devDependencies?.typescript);
|
|
1801
|
+
}
|
|
1802
|
+
function withPrepareScript(pkg) {
|
|
1803
|
+
return { ...pkg, scripts: { ...pkg.scripts, prepare: HOOKS_PATH_COMMAND } };
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/components/setup/setup.ts
|
|
1807
|
+
var SETUP_USAGE = "usage: gith setup -n npm [--force]";
|
|
1808
|
+
function setup(flags) {
|
|
1809
|
+
const unknown = [...flags.keys()].filter((key) => key !== "name" && key !== "force");
|
|
1810
|
+
if (unknown.length > 0 || flags.get("name") === undefined) {
|
|
1811
|
+
throw new Error(SETUP_USAGE);
|
|
1812
|
+
}
|
|
1813
|
+
const target = flags.get("name");
|
|
1814
|
+
if (target !== "npm") {
|
|
1815
|
+
throw new Error(`unknown setup '${target}': only npm is supported. ${SETUP_USAGE}`);
|
|
1816
|
+
}
|
|
1817
|
+
setupNpmHook({ force: flags.has("force") });
|
|
1818
|
+
}
|
|
1819
|
+
function setupNpmHook({ force }) {
|
|
1820
|
+
const root = process.cwd();
|
|
1821
|
+
if (!existsSync4(path3.join(root, ".git"))) {
|
|
1822
|
+
throw new Error("not a git repository root: run it in the folder that holds .git");
|
|
1823
|
+
}
|
|
1824
|
+
const packagePath = path3.join(root, "package.json");
|
|
1825
|
+
if (!existsSync4(packagePath))
|
|
1826
|
+
throw new Error("no package.json in this folder");
|
|
1827
|
+
const hookPath = path3.join(root, HOOK_FILE);
|
|
1828
|
+
if (existsSync4(hookPath) && !force) {
|
|
1829
|
+
throw new Error(`${HOOK_FILE} already exists. Pass --force to overwrite it.`);
|
|
1830
|
+
}
|
|
1831
|
+
const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
|
|
1832
|
+
const manager = detectPackageManager(root);
|
|
1833
|
+
const typescript = isTypeScriptProject(root, pkg);
|
|
1834
|
+
const hook = buildPreCommitHook(pkg, manager, typescript);
|
|
1835
|
+
if (isDryRun()) {
|
|
1836
|
+
console.log(`dry run: would write ${HOOK_FILE} (package manager: ${manager})`);
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
mkdirSync(path3.join(root, HOOKS_DIR), { recursive: true });
|
|
1840
|
+
writeFileSync2(hookPath, hook);
|
|
1841
|
+
try {
|
|
1842
|
+
chmodSync(hookPath, 493);
|
|
1843
|
+
} catch {}
|
|
1844
|
+
gitArgs(["config", "core.hooksPath", HOOKS_DIR]);
|
|
1845
|
+
if (pkg.scripts?.prepare !== HOOKS_PATH_COMMAND) {
|
|
1846
|
+
writeFileSync2(packagePath, `${JSON.stringify(withPrepareScript(pkg), null, 2)}
|
|
1847
|
+
`);
|
|
1848
|
+
console.log(`✏️ Added "prepare" → "${HOOKS_PATH_COMMAND}" to package.json`);
|
|
1849
|
+
}
|
|
1850
|
+
console.log(`✅ Wrote ${HOOK_FILE} and set core.hooksPath (package manager: ${manager}${typescript ? ", TypeScript" : ""})`);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1624
1853
|
// src/components/stash/stash.ts
|
|
1625
1854
|
function stash(flags) {
|
|
1626
1855
|
const name = flags.get("name");
|
|
@@ -1819,6 +2048,7 @@ function table(rows) {
|
|
|
1819
2048
|
`);
|
|
1820
2049
|
}
|
|
1821
2050
|
function stat(flags) {
|
|
2051
|
+
assertAllowedFlags(flags, [...PERIODS, "limit"], "stat");
|
|
1822
2052
|
const period = onlyOne(flags, PERIODS) ?? "overall";
|
|
1823
2053
|
const limit = parseLimit(flags);
|
|
1824
2054
|
const range = periodRange(period, new Date);
|
|
@@ -1958,7 +2188,7 @@ function tree(flags) {
|
|
|
1958
2188
|
}
|
|
1959
2189
|
|
|
1960
2190
|
// src/components/version/version.ts
|
|
1961
|
-
import { readFileSync as
|
|
2191
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1962
2192
|
|
|
1963
2193
|
// src/components/version/version.constants.ts
|
|
1964
2194
|
var MASCOT = [
|
|
@@ -1973,19 +2203,19 @@ var MASCOT = [
|
|
|
1973
2203
|
];
|
|
1974
2204
|
|
|
1975
2205
|
// src/components/version/version.utils.ts
|
|
1976
|
-
import { existsSync as
|
|
1977
|
-
import
|
|
2206
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
2207
|
+
import path4 from "node:path";
|
|
1978
2208
|
import { fileURLToPath } from "node:url";
|
|
1979
2209
|
function packageJsonPath() {
|
|
1980
|
-
let dir =
|
|
2210
|
+
let dir = path4.dirname(fileURLToPath(import.meta.url));
|
|
1981
2211
|
for (;; ) {
|
|
1982
|
-
const candidate =
|
|
1983
|
-
if (
|
|
1984
|
-
const pkg = JSON.parse(
|
|
2212
|
+
const candidate = path4.join(dir, "package.json");
|
|
2213
|
+
if (existsSync5(candidate)) {
|
|
2214
|
+
const pkg = JSON.parse(readFileSync4(candidate, "utf8"));
|
|
1985
2215
|
if (pkg.name === "@tertium/gith")
|
|
1986
2216
|
return candidate;
|
|
1987
2217
|
}
|
|
1988
|
-
const parent =
|
|
2218
|
+
const parent = path4.dirname(dir);
|
|
1989
2219
|
if (parent === dir)
|
|
1990
2220
|
throw new Error("gith package.json not found");
|
|
1991
2221
|
dir = parent;
|
|
@@ -1994,7 +2224,7 @@ function packageJsonPath() {
|
|
|
1994
2224
|
|
|
1995
2225
|
// src/components/version/version.ts
|
|
1996
2226
|
function readVersion() {
|
|
1997
|
-
const pkg = JSON.parse(
|
|
2227
|
+
const pkg = JSON.parse(readFileSync5(packageJsonPath(), "utf8"));
|
|
1998
2228
|
return pkg.version ?? "unknown";
|
|
1999
2229
|
}
|
|
2000
2230
|
function versionText() {
|
|
@@ -2011,24 +2241,25 @@ Start and finish work
|
|
|
2011
2241
|
gith create -b <branch> work branch develop--<branch> from develop (no switch)
|
|
2012
2242
|
gith create -r <version> release/<version> from develop
|
|
2013
2243
|
gith create -h <version> hotfix/<version> from main
|
|
2014
|
-
gith create --release | --hotfix version from the latest tag
|
|
2244
|
+
gith create --release | --hotfix next version from the latest tag
|
|
2015
2245
|
gith switch list local and origin branches, and switch to the one picked
|
|
2016
2246
|
gith switch -b <branch> switch to a branch, creating develop--<branch> from develop if missing (on a detached HEAD: <branch> from that commit)
|
|
2017
2247
|
gith switch -commit <sha> detach HEAD at a commit, to look at it (tracked files must be clean)
|
|
2018
|
-
gith switch --release | --hotfix switch to the newest release/* or hotfix/* (local or origin); -r
|
|
2248
|
+
gith switch --release | --hotfix switch to the newest release/* or hotfix/* (local or origin); -r <v> | -h <v> for one version
|
|
2019
2249
|
gith update merge origin/develop into this work branch
|
|
2020
2250
|
gith finish --current-release | --current-hotfix release the newest release/* or hotfix/*: version, changelog, merge to main, tag, back to develop
|
|
2021
2251
|
gith merge --develop merge this branch into develop
|
|
2022
2252
|
gith merge --main merge this branch into main
|
|
2023
2253
|
gith merge pick the branch to merge, then the target (main, develop, release/*, hotfix/*)
|
|
2024
|
-
gith merge
|
|
2254
|
+
gith merge --release | --hotfix merge into the only matching container, or pick one
|
|
2255
|
+
gith merge -r <v> | -h <v> merge into release/<v> or hotfix/<v>
|
|
2025
2256
|
gith merge --current-release merge this branch into the newest release (local or origin)
|
|
2026
2257
|
gith merge --current-hotfix merge this branch into the newest hotfix (local or origin)
|
|
2027
2258
|
gith release --patch | --minor | --major bump the version and publish
|
|
2028
2259
|
|
|
2029
2260
|
Fix history
|
|
2030
2261
|
gith undo undo the last commit, keep its changes staged (not if pushed)
|
|
2031
|
-
gith save
|
|
2262
|
+
gith save -m <msg> [--all] ["--&push"] new commit without prompts; push only with "--&push"
|
|
2032
2263
|
gith squash -limit <n> [-m <msg>] combine the last n commits into one (keeps the oldest message)
|
|
2033
2264
|
|
|
2034
2265
|
Save and reset work
|
|
@@ -2052,23 +2283,26 @@ Clean up branches (every removal shows what it will remove first)
|
|
|
2052
2283
|
|
|
2053
2284
|
Look at the repo
|
|
2054
2285
|
gith status branch vs origin, and staged, unstaged and untracked counts
|
|
2055
|
-
gith list --commits [--graph] [-l <n>] newest commits on this branch (default 5)
|
|
2286
|
+
gith list --commits [--graph] [-l <n>] newest commits on this branch; --graph adds colored lanes, refs, author/date (default 5)
|
|
2056
2287
|
gith list --tags [-l <n>] tags with a local and origin column (newest 5 by default)
|
|
2057
2288
|
gith list --stashes [-l <n>] stashes, newest first (default 5); --pockets shows them with Gorlum
|
|
2058
2289
|
gith show ... same as gith list
|
|
2059
2290
|
gith tree [-l <n>] working trees: path, branch, HEAD (current marked *)
|
|
2060
2291
|
gith find|search [-commit <text>] [-file <pattern>] [-change <text>] [--regex] [-limit <n>]
|
|
2061
2292
|
newest commits on this branch matching every filter (default 5)
|
|
2062
|
-
gith stat [
|
|
2063
|
-
|
|
2293
|
+
gith stat [--overall | --current-year | --current-month | --current-week | --last-year | --last-month | --last-week] [-limit <n>]
|
|
2294
|
+
fun facts about this branch: biggest commit, busiest day, streaks
|
|
2064
2295
|
|
|
2065
2296
|
Remote and health
|
|
2297
|
+
gith push [--all-branches] push to origin, set tracking, report ahead/behind; never force
|
|
2298
|
+
gith pull [--all-branches] fast-forward from origin; refuse diverged histories
|
|
2066
2299
|
gith sync fetch all remotes and tags, prune stale refs, then sync this branch with origin both ways
|
|
2067
2300
|
gith sync --develop | --main the same for develop or main
|
|
2068
2301
|
gith sync develop:develop two-way sync of one branch with origin: fetch it when behind, push it when ahead, stop when diverged
|
|
2069
2302
|
gith open open origin in the browser
|
|
2070
2303
|
gith doctor check the repo is ready for the branch flow (exit 1 on failures)
|
|
2071
2304
|
gith changelog [--dry-run] add an entry for package.json's version to CHANGELOG.md (commits since the last tag)
|
|
2305
|
+
gith setup -n npm [--force] write a shared pre-commit hook (lint, typecheck, test) for an npm project; --force overwrites it
|
|
2072
2306
|
|
|
2073
2307
|
Help
|
|
2074
2308
|
gith help show this help
|
|
@@ -2077,10 +2311,12 @@ Help
|
|
|
2077
2311
|
--dry-run (any command) print the git commands that would run, without running them
|
|
2078
2312
|
--yes (any command) answer yes to every question (for scripts)
|
|
2079
2313
|
|
|
2080
|
-
Flags:
|
|
2081
|
-
-b -r -h -l -m -n -limit -message -name -release -hotfix -
|
|
2082
|
-
--patch --minor --major --local --remote --obsolete --detached --tags --commits --graph --stashes --pockets
|
|
2083
|
-
--
|
|
2314
|
+
Flags: every single-dash flag requires a value; double-dash flags never take one.
|
|
2315
|
+
-b -r -h -l -m -n -limit -message -name -branch -release -hotfix -commit -file -change -days take values
|
|
2316
|
+
--patch --minor --major --local --remote --obsolete --detached --tags --commits --graph --stashes --pockets --branch --regex
|
|
2317
|
+
--develop --main --current-release --current-hotfix --all --&push --all-branches --no-commit --force
|
|
2318
|
+
--overall --current-year --current-month --current-week --last-year --last-month --last-week switches
|
|
2319
|
+
--branch lists removable branches; --release / --hotfix choose a version or branch by command context
|
|
2084
2320
|
-limit caps listings and stat (or -l); -days sets the age for remove --obsolete`;
|
|
2085
2321
|
var args = process.argv.slice(2).filter((arg) => {
|
|
2086
2322
|
if (arg === "--verbose")
|
|
@@ -2137,7 +2373,13 @@ try {
|
|
|
2137
2373
|
undo(parseFlags(rest));
|
|
2138
2374
|
break;
|
|
2139
2375
|
case "save":
|
|
2140
|
-
|
|
2376
|
+
save(parseFlags(rest));
|
|
2377
|
+
break;
|
|
2378
|
+
case "push":
|
|
2379
|
+
push(parseFlags(rest));
|
|
2380
|
+
break;
|
|
2381
|
+
case "pull":
|
|
2382
|
+
pull(parseFlags(rest));
|
|
2141
2383
|
break;
|
|
2142
2384
|
case "squash":
|
|
2143
2385
|
await squash(parseFlags(rest));
|
|
@@ -2161,6 +2403,9 @@ try {
|
|
|
2161
2403
|
case "changelog":
|
|
2162
2404
|
changelog(parseFlags(rest));
|
|
2163
2405
|
break;
|
|
2406
|
+
case "setup":
|
|
2407
|
+
setup(parseFlags(rest));
|
|
2408
|
+
break;
|
|
2164
2409
|
case "doctor":
|
|
2165
2410
|
if (!doctor(parseFlags(rest)))
|
|
2166
2411
|
exit(1);
|
|
@@ -2173,6 +2418,7 @@ try {
|
|
|
2173
2418
|
break;
|
|
2174
2419
|
case "release": {
|
|
2175
2420
|
const flags = parseFlags(rest);
|
|
2421
|
+
assertAllowedFlags(flags, ["patch", "minor", "major"], "release");
|
|
2176
2422
|
const kind = onlyOne(flags, ["patch", "minor", "major"]);
|
|
2177
2423
|
if (!kind)
|
|
2178
2424
|
throw new Error("usage: gith release --patch | --minor | --major");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tertium/gith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"release:patch": "node bin/gith.js release --patch",
|
|
23
23
|
"release:minor": "node bin/gith.js release --minor",
|
|
24
24
|
"release:major": "node bin/gith.js release --major",
|
|
25
|
-
"version": "bun run build && node bin/gith.js changelog"
|
|
25
|
+
"version": "bun run build && node bin/gith.js changelog --no-commit && git add -- CHANGELOG.md"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"author": "Vitalii Balabanov",
|