@tertium/gith 0.3.2 → 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 +41 -49
- package/bin/gith.js +361 -110
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @tertium/gith
|
|
2
2
|
|
|
3
|
+
gith: git-only branch flow CLI (goal branches, release/hotfix containers, synced merges).
|
|
4
|
+
|
|
3
5
|
```
|
|
4
6
|
o
|
|
5
7
|
/
|
|
@@ -11,22 +13,11 @@
|
|
|
11
13
|
d b
|
|
12
14
|
```
|
|
13
15
|
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
## Install
|
|
19
|
-
|
|
20
|
-
Needs [Node.js](https://nodejs.org) 18 or newer and git on the PATH.
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install -g @tertium/gith
|
|
24
|
-
gith --version
|
|
25
|
-
```
|
|
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).
|
|
26
17
|
|
|
27
|
-
|
|
18
|
+
See [Command design rules](docs/RULES.md) before changing or adding commands and flags.
|
|
28
19
|
|
|
29
|
-
|
|
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.
|
|
30
21
|
|
|
31
22
|
## Usage
|
|
32
23
|
|
|
@@ -35,20 +26,21 @@ From a clone of this repository, build the CLI and link it: `bun install`, `bun
|
|
|
35
26
|
gith create -b login-redirect # work branch develop--login-redirect from develop; stays on the current branch
|
|
36
27
|
gith create -r 1.2.0 # release/1.2.0 from develop
|
|
37
28
|
gith create -h 1.2.1 # hotfix/1.2.1 from main
|
|
38
|
-
gith create --release # release
|
|
39
|
-
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
|
|
40
31
|
gith switch # list local and origin branches, and switch to the one you pick
|
|
41
32
|
gith switch -b login-redirect # switch to develop--login-redirect, creating it from develop if it is missing
|
|
42
33
|
gith switch -commit 3f2a1b9 # detach HEAD at that commit, to look at old code
|
|
43
34
|
gith switch --release # switch to the newest release/* (local or origin); -r 1.9.0 for one version
|
|
44
35
|
gith switch --hotfix # switch to the newest hotfix/* (local or origin); -h 1.2.1 for one version
|
|
45
36
|
gith update # merge origin/develop into this work branch
|
|
46
|
-
gith merge
|
|
37
|
+
gith merge --develop # this branch into develop
|
|
47
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
|
|
48
39
|
gith finish --current-hotfix # the same for the newest hotfix/*
|
|
49
40
|
gith merge # pick the branch to merge, then the target from the list
|
|
50
41
|
gith merge --main # this branch into main
|
|
51
|
-
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
|
|
52
44
|
gith merge -h 1.2.1 # this branch into hotfix/1.2.1
|
|
53
45
|
gith merge --current-release # this branch into the newest release/* (local or origin)
|
|
54
46
|
gith merge --current-hotfix # this branch into the newest hotfix/* (local or origin)
|
|
@@ -58,7 +50,7 @@ gith release --major # npm version major, from develop into main
|
|
|
58
50
|
|
|
59
51
|
# Fix history
|
|
60
52
|
gith undo # undo the last commit, keep its changes staged (not if pushed)
|
|
61
|
-
gith save
|
|
53
|
+
gith save -m "msg" [--all] ["--&push"] # new commit without prompts; push only with "--&push"
|
|
62
54
|
gith squash -limit 3 [-m "msg"] # combine the last 3 commits into one (keeps the oldest message)
|
|
63
55
|
|
|
64
56
|
# Save and reset work
|
|
@@ -100,13 +92,20 @@ gith stat # fun facts about this branch: biggest commi
|
|
|
100
92
|
gith stat --last-week -limit 50 # the same for last week, looking at no more than 50 commits
|
|
101
93
|
|
|
102
94
|
# Remote and health
|
|
103
|
-
gith
|
|
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
|
|
97
|
+
gith sync # fetch all remotes and tags, prune stale refs, then sync this branch with origin both ways
|
|
98
|
+
gith sync --develop | --main # the same for develop or main
|
|
104
99
|
gith sync develop:develop # two-way sync of one branch with origin
|
|
105
100
|
gith open # open origin in the browser
|
|
106
101
|
gith doctor # check the repo is ready for the branch flow
|
|
107
102
|
gith changelog --dry-run # preview the CHANGELOG.md entry for package.json's version
|
|
108
103
|
gith changelog # write it as its own commit (docs(changelog): <version>); --no-commit only writes
|
|
109
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
|
+
|
|
110
109
|
# Help
|
|
111
110
|
gith help # full usage
|
|
112
111
|
gith --version # the version, with the mascot
|
|
@@ -117,34 +116,39 @@ gith list --tags --verbose
|
|
|
117
116
|
|
|
118
117
|
## Flags
|
|
119
118
|
|
|
120
|
-
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.
|
|
121
120
|
|
|
122
121
|
| Flag | Kind | Used by |
|
|
123
122
|
| --- | --- | --- |
|
|
124
123
|
| `-b <name>`, `-branch <name>` | value | `create`, `switch`, `remove` |
|
|
125
124
|
| `--branch` | alone: open the picker | `remove` |
|
|
126
|
-
| `-r <version>`, `-release <version>` | value | `create`, `merge` |
|
|
127
|
-
|
|
|
128
|
-
| `--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` |
|
|
129
127
|
| `--main` | alone: merge into main | `merge` |
|
|
130
128
|
| `--current-release`, `--current-hotfix` | switch: merge into the newest release or hotfix | `merge` |
|
|
131
|
-
| `-h <version>`, `-hotfix <version>` | value | `create`, `merge` |
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
| `-
|
|
135
|
-
|
|
|
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 |
|
|
136
136
|
| `-n <name>`, `-name <name>` | value | `stash` |
|
|
137
|
+
| `-commit <sha>` | value | `switch`: detach at a commit |
|
|
138
|
+
| `-file <pattern>`, `-change <text>` | value | `find` |
|
|
137
139
|
| `-l <n>`, `-limit <n>` | value, positive whole number | every listing: `list`, `tree`, the picker |
|
|
138
140
|
| `--patch`, `--minor`, `--major` | switch | `release` |
|
|
139
141
|
| `--overall`, `--current-year`, `--current-month`, `--current-week`, `--last-year`, `--last-month`, `--last-week` | switch, one at a time | `stat` |
|
|
140
142
|
| `--local`, `--remote` | switch | `remove` (both sides when neither is given) |
|
|
141
143
|
| `--obsolete`, `--detached` | switch | `remove` |
|
|
144
|
+
| `--force` | switch | `setup`: replace an existing hook |
|
|
145
|
+
| `--no-commit` | switch | `changelog`: update the file without committing |
|
|
142
146
|
| `-days <n>` | value | `remove --obsolete` |
|
|
143
147
|
| `--tags`, `--commits`, `--graph`, `--stashes`, `--pockets` | switch | `list` (also `show`) |
|
|
144
148
|
| `--verbose` | switch | every command |
|
|
145
149
|
| `--dry-run` | switch | every command |
|
|
146
150
|
|
|
147
|
-
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`.
|
|
148
152
|
|
|
149
153
|
`--verbose` and `--dry-run` can go anywhere on the line:
|
|
150
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.
|
|
@@ -159,7 +163,8 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
159
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.
|
|
160
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.
|
|
161
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.
|
|
162
|
-
- **`
|
|
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`.
|
|
163
168
|
- **`stash`** saves changes with `git stash push --include-untracked`. `-n` names the stash. It refuses when there is nothing to save.
|
|
164
169
|
- **`find`** (also `search`) lists the newest commits on this branch that match every filter given:
|
|
165
170
|
- `-commit <text>` matches the commit message, ignoring case, and the text is literal.
|
|
@@ -176,12 +181,13 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
176
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.
|
|
177
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.
|
|
178
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.
|
|
179
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.
|
|
180
|
-
- **`sync`** runs `git fetch --all --tags --prune
|
|
181
|
-
- **`sync <branch>:<branch>`** (for example `sync develop:develop`)
|
|
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.
|
|
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.
|
|
182
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.
|
|
183
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.
|
|
184
|
-
- **`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.
|
|
185
191
|
- **`clean`** runs `git clean -fd`. It permanently removes untracked files and directories, but keeps ignored files.
|
|
186
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.
|
|
187
193
|
|
|
@@ -202,7 +208,7 @@ Containers (`develop`, `main`, `release/*`, `hotfix/*`) can't be merged from, an
|
|
|
202
208
|
Run as `gith release --patch`, `--minor` or `--major`.
|
|
203
209
|
|
|
204
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.
|
|
205
|
-
- **`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`.
|
|
206
212
|
|
|
207
213
|
Both need a clean working tree and a `package.json` with a version, because they call `npm version`.
|
|
208
214
|
|
|
@@ -210,20 +216,6 @@ Both need a clean working tree and a `package.json` with a version, because they
|
|
|
210
216
|
|
|
211
217
|
Each failure exits with code 1 and a message. Conflicts leave you on the work branch with the merge aborted, so you can resolve and retry.
|
|
212
218
|
|
|
213
|
-
## Development
|
|
214
|
-
|
|
215
|
-
```bash
|
|
216
|
-
bun install
|
|
217
|
-
bun run test # unit tests
|
|
218
|
-
bun run lint # biome check
|
|
219
|
-
bun run lint:fix # biome check --write
|
|
220
|
-
bun run typecheck # tsc --noEmit
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## Publishing
|
|
224
|
-
|
|
225
|
-
The package ships one built file, `bin/gith.js`, which runs on Node. `npm pack --dry-run` lists what would be published. `prepublishOnly` runs lint, typecheck, the tests and the build before any publish. `.github/workflows/publish.yml` runs the same checks, then `npm publish --access public`, whenever a `v*` tag is pushed. It uses npm trusted publishing (OIDC), so no npm token is stored in the repo. Bump the version with `gith release` or `npm version`, which creates the tag.
|
|
226
|
-
|
|
227
219
|
## Support
|
|
228
220
|
|
|
229
221
|
If this project is useful to you, you can support its development on [Ko-fi](https://ko-fi.com/tertiumnon).
|
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 {
|
|
@@ -474,8 +480,10 @@ async function confirm(question) {
|
|
|
474
480
|
console.log(`dry run: would ask: ${question}`);
|
|
475
481
|
return true;
|
|
476
482
|
}
|
|
477
|
-
if (assumeYes)
|
|
483
|
+
if (assumeYes) {
|
|
484
|
+
console.log(`${question}yes (--yes)`);
|
|
478
485
|
return true;
|
|
486
|
+
}
|
|
479
487
|
if (!process.stdin.isTTY)
|
|
480
488
|
throw new Error("this needs a terminal to confirm; pass --yes to skip the question.");
|
|
481
489
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -486,20 +494,6 @@ async function confirm(question) {
|
|
|
486
494
|
rl.close();
|
|
487
495
|
}
|
|
488
496
|
}
|
|
489
|
-
async function ask(question) {
|
|
490
|
-
if (isDryRun()) {
|
|
491
|
-
console.log(`dry run: would ask: ${question}`);
|
|
492
|
-
return "<message>";
|
|
493
|
-
}
|
|
494
|
-
if (!process.stdin.isTTY)
|
|
495
|
-
throw new Error("needs a terminal to ask; pass -m <message>.");
|
|
496
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
497
|
-
try {
|
|
498
|
-
return (await rl.question(question)).trim();
|
|
499
|
-
} finally {
|
|
500
|
-
rl.close();
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
497
|
async function select(question, options, multi) {
|
|
504
498
|
if (!process.stdin.isTTY)
|
|
505
499
|
throw new Error("this needs a terminal to pick.");
|
|
@@ -658,9 +652,10 @@ function open(kind, version) {
|
|
|
658
652
|
|
|
659
653
|
// src/components/flow/flow.ts
|
|
660
654
|
function create(flags) {
|
|
655
|
+
assertAllowedFlags(flags, ["branch", "release", "hotfix"], "create");
|
|
661
656
|
const kind = onlyOne(flags, ["branch", "release", "hotfix"]);
|
|
662
657
|
if (!kind)
|
|
663
|
-
throw new Error("usage: gith create -b <branch> | -r
|
|
658
|
+
throw new Error("usage: gith create -b <branch> | -r <version> | -h <version> | --release | --hotfix");
|
|
664
659
|
const value = flags.get(kind) ?? "";
|
|
665
660
|
if (kind === "branch") {
|
|
666
661
|
if (!value)
|
|
@@ -671,6 +666,14 @@ function create(flags) {
|
|
|
671
666
|
open(kind, value || nextContainerVersion(kind));
|
|
672
667
|
}
|
|
673
668
|
async function merge(flags) {
|
|
669
|
+
assertAllowedFlags(flags, [
|
|
670
|
+
"develop",
|
|
671
|
+
"main",
|
|
672
|
+
"release",
|
|
673
|
+
"hotfix",
|
|
674
|
+
"current-release",
|
|
675
|
+
"current-hotfix"
|
|
676
|
+
], "merge");
|
|
674
677
|
if (flags.size === 0)
|
|
675
678
|
return mergePick();
|
|
676
679
|
const kind = onlyOne(flags, [
|
|
@@ -890,15 +893,109 @@ function assertNotPushed() {
|
|
|
890
893
|
throw new Error("HEAD is already on origin; changing it would need a force-push.");
|
|
891
894
|
}
|
|
892
895
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
if (
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
+
};
|
|
902
999
|
|
|
903
1000
|
// src/components/history/history.ts
|
|
904
1001
|
function update(flags) {
|
|
@@ -924,50 +1021,33 @@ function undo(flags) {
|
|
|
924
1021
|
assertNotPushed();
|
|
925
1022
|
gitArgs(["reset", "--soft", "HEAD~1"]);
|
|
926
1023
|
}
|
|
927
|
-
|
|
1024
|
+
var save = (flags) => {
|
|
928
1025
|
const message = flags.get("message");
|
|
929
|
-
const extra = [...flags.keys()].filter((key) => key !== "message" && key !== "all");
|
|
930
|
-
if (extra.length > 0)
|
|
931
|
-
throw new Error(
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const remote = remoteSha(branch);
|
|
942
|
-
if (rewrite && remote === "") {
|
|
943
|
-
throw new Error("HEAD is already on origin under another branch; changing it would need a force-push.");
|
|
944
|
-
}
|
|
945
|
-
if (rewrite) {
|
|
946
|
-
const yes = await confirm(`HEAD is already on origin/${branch}. Save it and force-push ${branch}? [Y/n] `);
|
|
947
|
-
if (!yes) {
|
|
948
|
-
console.log("save cancelled; nothing changed.");
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
if (owned) {
|
|
953
|
-
gitArgs(message ? ["commit", "--amend", "-m", message] : ["commit", "--amend", "--no-edit"]);
|
|
954
|
-
} else {
|
|
955
|
-
const text = message ?? await askMessage();
|
|
956
|
-
gitArgs(["commit", "-m", text]);
|
|
957
|
-
}
|
|
958
|
-
if (remote === "") {
|
|
959
|
-
console.log(`origin has no ${branch}; not pushed.`);
|
|
960
|
-
return;
|
|
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();
|
|
1030
|
+
const unstaged = captureArgs(["add", "-A", "--dry-run"]).split(`
|
|
1031
|
+
`).filter(Boolean);
|
|
1032
|
+
const stageAll = flags.has("all") || unstaged.length === 1;
|
|
1033
|
+
if (stageAll && unstaged.length > 0) {
|
|
1034
|
+
process.stdout.write(`Staging ${unstaged.length} unstaged change(s):
|
|
1035
|
+
${unstaged.join(`
|
|
1036
|
+
`)}
|
|
1037
|
+
`);
|
|
961
1038
|
}
|
|
962
|
-
if (
|
|
963
|
-
|
|
964
|
-
|
|
1039
|
+
if (stageAll)
|
|
1040
|
+
gitArgs(["add", "-A"]);
|
|
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.");
|
|
965
1043
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
+
};
|
|
971
1051
|
async function squash(flags) {
|
|
972
1052
|
const count = parseLimit(flags);
|
|
973
1053
|
const message = flags.get("message");
|
|
@@ -1009,17 +1089,19 @@ async function squash(flags) {
|
|
|
1009
1089
|
else
|
|
1010
1090
|
gitArgs(["commit", "-C", oldest]);
|
|
1011
1091
|
}
|
|
1012
|
-
function hasOwnCommit() {
|
|
1013
|
-
for (const base of ["origin/develop", "origin/main"]) {
|
|
1014
|
-
try {
|
|
1015
|
-
return captureArgs(["rev-list", "--count", `${base}..HEAD`]) !== "0";
|
|
1016
|
-
} catch {}
|
|
1017
|
-
}
|
|
1018
|
-
return true;
|
|
1019
|
-
}
|
|
1020
1092
|
|
|
1021
1093
|
// src/components/list/list.constants.ts
|
|
1022
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
|
+
];
|
|
1023
1105
|
var GORLUM = [
|
|
1024
1106
|
' .-"""-.',
|
|
1025
1107
|
" / o o \\",
|
|
@@ -1032,14 +1114,35 @@ var GORLUM = [
|
|
|
1032
1114
|
function yesNo(value) {
|
|
1033
1115
|
return value ? "yes" : "no";
|
|
1034
1116
|
}
|
|
1035
|
-
|
|
1117
|
+
var commits = (limit, graph) => {
|
|
1036
1118
|
if (!hasHead()) {
|
|
1037
|
-
|
|
1119
|
+
process.stdout.write(`No commits.
|
|
1120
|
+
`);
|
|
1038
1121
|
return;
|
|
1039
1122
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
+
};
|
|
1043
1146
|
function hasHead() {
|
|
1044
1147
|
try {
|
|
1045
1148
|
captureArgs(["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
@@ -1069,6 +1172,7 @@ function tagPairs(local, origin) {
|
|
|
1069
1172
|
});
|
|
1070
1173
|
}
|
|
1071
1174
|
function list(flags) {
|
|
1175
|
+
assertAllowedFlags(flags, ["tags", "commits", "stashes", "pockets", "graph", "limit"], "list");
|
|
1072
1176
|
const kind = onlyOne(flags, ["tags", "commits", "stashes", "pockets"]);
|
|
1073
1177
|
if (!kind)
|
|
1074
1178
|
throw new Error("usage: gith list --tags | --commits [--graph] | --stashes | --pockets [-limit <n>]");
|
|
@@ -1201,6 +1305,7 @@ async function switchBranch(flags) {
|
|
|
1201
1305
|
switchToCommit(commit);
|
|
1202
1306
|
return;
|
|
1203
1307
|
}
|
|
1308
|
+
assertAllowedFlags(flags, ["branch"], "switch");
|
|
1204
1309
|
const branch = requireBranch(flags, "switch");
|
|
1205
1310
|
const from = isDetached() ? "HEAD" : undefined;
|
|
1206
1311
|
const exists = (name) => hasLocal(name) || hasOnOrigin(name);
|
|
@@ -1267,6 +1372,16 @@ function detachedOptions(times, current) {
|
|
|
1267
1372
|
return options.sort((x, y) => x.name.localeCompare(y.name));
|
|
1268
1373
|
}
|
|
1269
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");
|
|
1270
1385
|
const name = flags.get("branch") ?? "";
|
|
1271
1386
|
const side = onlyOne(flags, ["local", "remote"]);
|
|
1272
1387
|
const scope = side ?? "pair";
|
|
@@ -1299,15 +1414,25 @@ async function remove(flags) {
|
|
|
1299
1414
|
await removeBranches(scope, await pick(scope, parseLimit(flags)));
|
|
1300
1415
|
}
|
|
1301
1416
|
function sync(flags) {
|
|
1302
|
-
|
|
1303
|
-
|
|
1417
|
+
const kind = onlyOne(flags, ["develop", "main"]);
|
|
1418
|
+
const extra = [...flags.keys()].filter((key) => key !== kind);
|
|
1419
|
+
if (extra.length > 0)
|
|
1420
|
+
throw new Error("usage: gith sync [--develop | --main]");
|
|
1421
|
+
const name = kind ?? currentBranch2();
|
|
1422
|
+
if (name === "HEAD")
|
|
1423
|
+
throw new Error("HEAD is detached; check out a branch, or use gith sync --develop | --main.");
|
|
1424
|
+
syncWithOrigin(name);
|
|
1304
1425
|
}
|
|
1305
1426
|
function syncBranch(args) {
|
|
1306
1427
|
const [spec = ""] = args;
|
|
1307
1428
|
const [name = "", other = name] = spec.split(":");
|
|
1308
1429
|
if (args.length !== 1 || other !== name || name === "")
|
|
1309
1430
|
throw new Error("usage: gith sync <branch>:<branch>, the same name on both sides.");
|
|
1431
|
+
syncWithOrigin(name);
|
|
1432
|
+
}
|
|
1433
|
+
function syncWithOrigin(name) {
|
|
1310
1434
|
assertBranchName(name);
|
|
1435
|
+
gitArgs(["fetch", "--all", "--tags", "--prune"]);
|
|
1311
1436
|
const local = hasLocal(name);
|
|
1312
1437
|
const remote = hasOnOrigin(name);
|
|
1313
1438
|
if (!local && !remote)
|
|
@@ -1316,7 +1441,6 @@ function syncBranch(args) {
|
|
|
1316
1441
|
gitArgs(["push", "origin", `${name}:${name}`]);
|
|
1317
1442
|
return;
|
|
1318
1443
|
}
|
|
1319
|
-
gitArgs(["fetch", "origin", name]);
|
|
1320
1444
|
if (!local) {
|
|
1321
1445
|
gitArgs(["fetch", "origin", `${name}:${name}`]);
|
|
1322
1446
|
return;
|
|
@@ -1617,6 +1741,115 @@ function releaseFromDevelop(kind) {
|
|
|
1617
1741
|
`);
|
|
1618
1742
|
}
|
|
1619
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
|
+
|
|
1620
1853
|
// src/components/stash/stash.ts
|
|
1621
1854
|
function stash(flags) {
|
|
1622
1855
|
const name = flags.get("name");
|
|
@@ -1815,6 +2048,7 @@ function table(rows) {
|
|
|
1815
2048
|
`);
|
|
1816
2049
|
}
|
|
1817
2050
|
function stat(flags) {
|
|
2051
|
+
assertAllowedFlags(flags, [...PERIODS, "limit"], "stat");
|
|
1818
2052
|
const period = onlyOne(flags, PERIODS) ?? "overall";
|
|
1819
2053
|
const limit = parseLimit(flags);
|
|
1820
2054
|
const range = periodRange(period, new Date);
|
|
@@ -1954,7 +2188,7 @@ function tree(flags) {
|
|
|
1954
2188
|
}
|
|
1955
2189
|
|
|
1956
2190
|
// src/components/version/version.ts
|
|
1957
|
-
import { readFileSync as
|
|
2191
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1958
2192
|
|
|
1959
2193
|
// src/components/version/version.constants.ts
|
|
1960
2194
|
var MASCOT = [
|
|
@@ -1969,19 +2203,19 @@ var MASCOT = [
|
|
|
1969
2203
|
];
|
|
1970
2204
|
|
|
1971
2205
|
// src/components/version/version.utils.ts
|
|
1972
|
-
import { existsSync as
|
|
1973
|
-
import
|
|
2206
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
2207
|
+
import path4 from "node:path";
|
|
1974
2208
|
import { fileURLToPath } from "node:url";
|
|
1975
2209
|
function packageJsonPath() {
|
|
1976
|
-
let dir =
|
|
2210
|
+
let dir = path4.dirname(fileURLToPath(import.meta.url));
|
|
1977
2211
|
for (;; ) {
|
|
1978
|
-
const candidate =
|
|
1979
|
-
if (
|
|
1980
|
-
const pkg = JSON.parse(
|
|
2212
|
+
const candidate = path4.join(dir, "package.json");
|
|
2213
|
+
if (existsSync5(candidate)) {
|
|
2214
|
+
const pkg = JSON.parse(readFileSync4(candidate, "utf8"));
|
|
1981
2215
|
if (pkg.name === "@tertium/gith")
|
|
1982
2216
|
return candidate;
|
|
1983
2217
|
}
|
|
1984
|
-
const parent =
|
|
2218
|
+
const parent = path4.dirname(dir);
|
|
1985
2219
|
if (parent === dir)
|
|
1986
2220
|
throw new Error("gith package.json not found");
|
|
1987
2221
|
dir = parent;
|
|
@@ -1990,7 +2224,7 @@ function packageJsonPath() {
|
|
|
1990
2224
|
|
|
1991
2225
|
// src/components/version/version.ts
|
|
1992
2226
|
function readVersion() {
|
|
1993
|
-
const pkg = JSON.parse(
|
|
2227
|
+
const pkg = JSON.parse(readFileSync5(packageJsonPath(), "utf8"));
|
|
1994
2228
|
return pkg.version ?? "unknown";
|
|
1995
2229
|
}
|
|
1996
2230
|
function versionText() {
|
|
@@ -2007,24 +2241,25 @@ Start and finish work
|
|
|
2007
2241
|
gith create -b <branch> work branch develop--<branch> from develop (no switch)
|
|
2008
2242
|
gith create -r <version> release/<version> from develop
|
|
2009
2243
|
gith create -h <version> hotfix/<version> from main
|
|
2010
|
-
gith create --release | --hotfix version from the latest tag
|
|
2244
|
+
gith create --release | --hotfix next version from the latest tag
|
|
2011
2245
|
gith switch list local and origin branches, and switch to the one picked
|
|
2012
2246
|
gith switch -b <branch> switch to a branch, creating develop--<branch> from develop if missing (on a detached HEAD: <branch> from that commit)
|
|
2013
2247
|
gith switch -commit <sha> detach HEAD at a commit, to look at it (tracked files must be clean)
|
|
2014
|
-
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
|
|
2015
2249
|
gith update merge origin/develop into this work branch
|
|
2016
2250
|
gith finish --current-release | --current-hotfix release the newest release/* or hotfix/*: version, changelog, merge to main, tag, back to develop
|
|
2017
2251
|
gith merge --develop merge this branch into develop
|
|
2018
2252
|
gith merge --main merge this branch into main
|
|
2019
2253
|
gith merge pick the branch to merge, then the target (main, develop, release/*, hotfix/*)
|
|
2020
|
-
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>
|
|
2021
2256
|
gith merge --current-release merge this branch into the newest release (local or origin)
|
|
2022
2257
|
gith merge --current-hotfix merge this branch into the newest hotfix (local or origin)
|
|
2023
2258
|
gith release --patch | --minor | --major bump the version and publish
|
|
2024
2259
|
|
|
2025
2260
|
Fix history
|
|
2026
2261
|
gith undo undo the last commit, keep its changes staged (not if pushed)
|
|
2027
|
-
gith save
|
|
2262
|
+
gith save -m <msg> [--all] ["--&push"] new commit without prompts; push only with "--&push"
|
|
2028
2263
|
gith squash -limit <n> [-m <msg>] combine the last n commits into one (keeps the oldest message)
|
|
2029
2264
|
|
|
2030
2265
|
Save and reset work
|
|
@@ -2048,22 +2283,26 @@ Clean up branches (every removal shows what it will remove first)
|
|
|
2048
2283
|
|
|
2049
2284
|
Look at the repo
|
|
2050
2285
|
gith status branch vs origin, and staged, unstaged and untracked counts
|
|
2051
|
-
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)
|
|
2052
2287
|
gith list --tags [-l <n>] tags with a local and origin column (newest 5 by default)
|
|
2053
2288
|
gith list --stashes [-l <n>] stashes, newest first (default 5); --pockets shows them with Gorlum
|
|
2054
2289
|
gith show ... same as gith list
|
|
2055
2290
|
gith tree [-l <n>] working trees: path, branch, HEAD (current marked *)
|
|
2056
2291
|
gith find|search [-commit <text>] [-file <pattern>] [-change <text>] [--regex] [-limit <n>]
|
|
2057
2292
|
newest commits on this branch matching every filter (default 5)
|
|
2058
|
-
gith stat [
|
|
2059
|
-
|
|
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
|
|
2060
2295
|
|
|
2061
2296
|
Remote and health
|
|
2062
|
-
gith
|
|
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
|
|
2299
|
+
gith sync fetch all remotes and tags, prune stale refs, then sync this branch with origin both ways
|
|
2300
|
+
gith sync --develop | --main the same for develop or main
|
|
2063
2301
|
gith sync develop:develop two-way sync of one branch with origin: fetch it when behind, push it when ahead, stop when diverged
|
|
2064
2302
|
gith open open origin in the browser
|
|
2065
2303
|
gith doctor check the repo is ready for the branch flow (exit 1 on failures)
|
|
2066
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
|
|
2067
2306
|
|
|
2068
2307
|
Help
|
|
2069
2308
|
gith help show this help
|
|
@@ -2072,10 +2311,12 @@ Help
|
|
|
2072
2311
|
--dry-run (any command) print the git commands that would run, without running them
|
|
2073
2312
|
--yes (any command) answer yes to every question (for scripts)
|
|
2074
2313
|
|
|
2075
|
-
Flags:
|
|
2076
|
-
-b -r -h -l -m -n -limit -message -name -release -hotfix -
|
|
2077
|
-
--patch --minor --major --local --remote --obsolete --detached --tags --commits --graph --stashes --pockets
|
|
2078
|
-
--
|
|
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
|
|
2079
2320
|
-limit caps listings and stat (or -l); -days sets the age for remove --obsolete`;
|
|
2080
2321
|
var args = process.argv.slice(2).filter((arg) => {
|
|
2081
2322
|
if (arg === "--verbose")
|
|
@@ -2132,7 +2373,13 @@ try {
|
|
|
2132
2373
|
undo(parseFlags(rest));
|
|
2133
2374
|
break;
|
|
2134
2375
|
case "save":
|
|
2135
|
-
|
|
2376
|
+
save(parseFlags(rest));
|
|
2377
|
+
break;
|
|
2378
|
+
case "push":
|
|
2379
|
+
push(parseFlags(rest));
|
|
2380
|
+
break;
|
|
2381
|
+
case "pull":
|
|
2382
|
+
pull(parseFlags(rest));
|
|
2136
2383
|
break;
|
|
2137
2384
|
case "squash":
|
|
2138
2385
|
await squash(parseFlags(rest));
|
|
@@ -2156,6 +2403,9 @@ try {
|
|
|
2156
2403
|
case "changelog":
|
|
2157
2404
|
changelog(parseFlags(rest));
|
|
2158
2405
|
break;
|
|
2406
|
+
case "setup":
|
|
2407
|
+
setup(parseFlags(rest));
|
|
2408
|
+
break;
|
|
2159
2409
|
case "doctor":
|
|
2160
2410
|
if (!doctor(parseFlags(rest)))
|
|
2161
2411
|
exit(1);
|
|
@@ -2168,6 +2418,7 @@ try {
|
|
|
2168
2418
|
break;
|
|
2169
2419
|
case "release": {
|
|
2170
2420
|
const flags = parseFlags(rest);
|
|
2421
|
+
assertAllowedFlags(flags, ["patch", "minor", "major"], "release");
|
|
2171
2422
|
const kind = onlyOne(flags, ["patch", "minor", "major"]);
|
|
2172
2423
|
if (!kind)
|
|
2173
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",
|