@tertium/gith 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -34
- package/bin/gith.js +486 -149
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
# @tertium/gith
|
|
2
2
|
|
|
3
|
+
<img src="assets/gith-icon.svg" alt="gith mascot with three commit sprouts" width="128" />
|
|
4
|
+
|
|
3
5
|
gith: git-only branch flow CLI (goal branches, release/hotfix containers, synced merges).
|
|
4
6
|
|
|
5
7
|
```
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
o----o----o
|
|
8
|
+
o
|
|
9
|
+
o | o
|
|
9
10
|
\ | /
|
|
10
11
|
\ | /
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
\ | /
|
|
13
|
+
\|/
|
|
14
|
+
|
|
|
15
|
+
.---.
|
|
16
|
+
( o o ) gith
|
|
17
|
+
'---' git happens.
|
|
18
|
+
|
|
|
19
|
+
/|\
|
|
20
|
+
/ \
|
|
14
21
|
```
|
|
15
22
|
|
|
16
|
-
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).
|
|
23
|
+
Git helper: the branch flow from [git-convention](https://github.com/Tertiumnon/git-convention) (work branches named develop--<goal>, `release/*` and `hotfix/*` containers, merges that sync both sides with origin), plus everyday git commands: status, switch, update, undo, save, squash, stash, remove, list, tree, push, pull, sync, clean, reset, open, doctor and stat. Any command takes `--dry-run` to show the git commands it would run. No dependencies beyond git and [Bun](https://bun.com).
|
|
24
|
+
|
|
25
|
+
See [Command design rules](docs/RULES.md) before changing or adding commands and flags.
|
|
26
|
+
|
|
27
|
+
See [CHANGELOG.md](CHANGELOG.md) for user-facing release notes and the changes included in each version. GitHub Releases are also published for tagged versions.
|
|
17
28
|
|
|
18
29
|
> **Very early state (0.1.0).** Commands and flags may change without notice, and the tool has only been run against throwaway test repositories. Check the result with `git log` and `git status` before you push.
|
|
19
30
|
|
|
@@ -24,30 +35,33 @@ Git helper: the branch flow from [git-convention](https://github.com/Tertiumnon/
|
|
|
24
35
|
gith create -b login-redirect # work branch develop--login-redirect from develop; stays on the current branch
|
|
25
36
|
gith create -r 1.2.0 # release/1.2.0 from develop
|
|
26
37
|
gith create -h 1.2.1 # hotfix/1.2.1 from main
|
|
27
|
-
gith create --release # release
|
|
28
|
-
gith create --hotfix # hotfix
|
|
38
|
+
gith create --release # next release version from the latest tag
|
|
39
|
+
gith create --hotfix # next hotfix version from the latest tag
|
|
40
|
+
gith create -r 1.2.0 --update-version --npm # also set npm version to 1.2.0 on the new release branch
|
|
41
|
+
gith create --hotfix --update-version --npm # same for an automatically selected hotfix version
|
|
29
42
|
gith switch # list local and origin branches, and switch to the one you pick
|
|
30
43
|
gith switch -b login-redirect # switch to develop--login-redirect, creating it from develop if it is missing
|
|
31
44
|
gith switch -commit 3f2a1b9 # detach HEAD at that commit, to look at old code
|
|
32
45
|
gith switch --release # switch to the newest release/* (local or origin); -r 1.9.0 for one version
|
|
33
46
|
gith switch --hotfix # switch to the newest hotfix/* (local or origin); -h 1.2.1 for one version
|
|
34
47
|
gith update # merge origin/develop into this work branch
|
|
35
|
-
gith merge
|
|
48
|
+
gith merge --develop # this branch into develop
|
|
36
49
|
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
50
|
gith finish --current-hotfix # the same for the newest hotfix/*
|
|
38
51
|
gith merge # pick the branch to merge, then the target from the list
|
|
39
52
|
gith merge --main # this branch into main
|
|
40
|
-
gith merge -r 1.2.0 # this branch into release/1.2.0
|
|
53
|
+
gith merge -r 1.2.0 # this branch into release/1.2.0
|
|
54
|
+
gith merge --release # this branch into the only release, or pick one
|
|
41
55
|
gith merge -h 1.2.1 # this branch into hotfix/1.2.1
|
|
42
56
|
gith merge --current-release # this branch into the newest release/* (local or origin)
|
|
43
57
|
gith merge --current-hotfix # this branch into the newest hotfix/* (local or origin)
|
|
44
|
-
gith release --patch #
|
|
45
|
-
gith release --minor #
|
|
46
|
-
gith release --major
|
|
58
|
+
gith release --patch # sync main into develop without changing package versions
|
|
59
|
+
gith release --minor # merge develop into main and sync back
|
|
60
|
+
gith release --major --update-version --npm # bump npm major version and release through main
|
|
47
61
|
|
|
48
62
|
# Fix history
|
|
49
63
|
gith undo # undo the last commit, keep its changes staged (not if pushed)
|
|
50
|
-
gith save
|
|
64
|
+
gith save -m "msg" [--all] ["--&push"] # new commit without prompts; push only with "--&push"
|
|
51
65
|
gith squash -limit 3 [-m "msg"] # combine the last 3 commits into one (keeps the oldest message)
|
|
52
66
|
|
|
53
67
|
# Save and reset work
|
|
@@ -72,14 +86,14 @@ gith remove --detached # pick local branches that have no origin co
|
|
|
72
86
|
gith remove --detached --all # remove all of them: lists them, then asks Y/n
|
|
73
87
|
|
|
74
88
|
# Look at the repo
|
|
75
|
-
gith status #
|
|
89
|
+
gith status # working-tree summary, all local and remote-tracking branches
|
|
76
90
|
gith list --commits # the last 5 commits on this branch
|
|
77
91
|
gith list --commits --graph # the same, as a branch graph
|
|
78
92
|
gith list --commits -l 10 # the last 10 (-limit 10 works too)
|
|
79
93
|
gith list --tags # the newest 5 tags with a LOCAL and ORIGIN column
|
|
80
94
|
gith list --tags -l 20 # the newest 20
|
|
81
95
|
gith list --stashes # the newest 5 stashes (-l for more)
|
|
82
|
-
gith show --pockets # the same, with
|
|
96
|
+
gith show --pockets # the same, with the gith mascot: my precious pockets!
|
|
83
97
|
gith find -commit "login" -limit 10 # commits whose message mentions login (search is the same; default 5)
|
|
84
98
|
gith search -file "*api.ts" -limit 3 # commits that changed a matching file, with the files listed
|
|
85
99
|
gith find -change "TODO" # commits that added or removed the text in their diff
|
|
@@ -89,6 +103,8 @@ gith stat # fun facts about this branch: biggest commi
|
|
|
89
103
|
gith stat --last-week -limit 50 # the same for last week, looking at no more than 50 commits
|
|
90
104
|
|
|
91
105
|
# Remote and health
|
|
106
|
+
gith push [--all-branches] # push to origin with tracking and ahead/behind counts; never force
|
|
107
|
+
gith pull [--all-branches] # fast-forward from origin; stop on divergence
|
|
92
108
|
gith sync # fetch all remotes and tags, prune stale refs, then sync this branch with origin both ways
|
|
93
109
|
gith sync --develop | --main # the same for develop or main
|
|
94
110
|
gith sync develop:develop # two-way sync of one branch with origin
|
|
@@ -97,6 +113,10 @@ gith doctor # check the repo is ready for the branch flo
|
|
|
97
113
|
gith changelog --dry-run # preview the CHANGELOG.md entry for package.json's version
|
|
98
114
|
gith changelog # write it as its own commit (docs(changelog): <version>); --no-commit only writes
|
|
99
115
|
|
|
116
|
+
# Set up
|
|
117
|
+
gith setup -n npm # write .githooks/pre-commit (lint, typecheck, test) and set core.hooksPath
|
|
118
|
+
gith setup -n npm --force # overwrite an existing hook
|
|
119
|
+
|
|
100
120
|
# Help
|
|
101
121
|
gith help # full usage
|
|
102
122
|
gith --version # the version, with the mascot
|
|
@@ -107,34 +127,40 @@ gith list --tags --verbose
|
|
|
107
127
|
|
|
108
128
|
## Flags
|
|
109
129
|
|
|
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,
|
|
130
|
+
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
131
|
|
|
112
132
|
| Flag | Kind | Used by |
|
|
113
133
|
| --- | --- | --- |
|
|
114
134
|
| `-b <name>`, `-branch <name>` | value | `create`, `switch`, `remove` |
|
|
115
135
|
| `--branch` | alone: open the picker | `remove` |
|
|
116
|
-
| `-r <version>`, `-release <version>` | value | `create`, `merge` |
|
|
117
|
-
|
|
|
118
|
-
| `--release` | alone: version from the latest tag | `create` |
|
|
136
|
+
| `-r <version>`, `-release <version>` | value | `create`, `switch`, `merge` |
|
|
137
|
+
| `--release` | automatic version in `create`; newest container in `switch`; only container or picker in `merge` | `create`, `switch`, `merge` |
|
|
119
138
|
| `--main` | alone: merge into main | `merge` |
|
|
120
139
|
| `--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
|
-
|
|
|
140
|
+
| `-h <version>`, `-hotfix <version>` | value | `create`, `switch`, `merge` |
|
|
141
|
+
| `--hotfix` | automatic version in `create`; newest container in `switch`; only container or picker in `merge` | `create`, `switch`, `merge` |
|
|
142
|
+
| `-limit <n>` | value | `squash`: commits to combine; listings and `stat`: rows or commits to show |
|
|
143
|
+
| `-m <msg>`, `-message <msg>` | value | `save` (required), `squash` |
|
|
144
|
+
| `--all` | switch | `save`: stage all changes |
|
|
145
|
+
| `"--&push"` | switch | `save`: push after committing |
|
|
146
|
+
| `--all-branches` | switch | `push`, `pull`: all local branches |
|
|
126
147
|
| `-n <name>`, `-name <name>` | value | `stash` |
|
|
148
|
+
| `-commit <sha>` | value | `switch`: detach at a commit |
|
|
149
|
+
| `-file <pattern>`, `-change <text>` | value | `find` |
|
|
127
150
|
| `-l <n>`, `-limit <n>` | value, positive whole number | every listing: `list`, `tree`, the picker |
|
|
128
151
|
| `--patch`, `--minor`, `--major` | switch | `release` |
|
|
152
|
+
| `--update-version`, `--npm` | switches, required together for npm version updates | `release`, `create` with `-r`/`-h`/`--release`/`--hotfix` |
|
|
129
153
|
| `--overall`, `--current-year`, `--current-month`, `--current-week`, `--last-year`, `--last-month`, `--last-week` | switch, one at a time | `stat` |
|
|
130
154
|
| `--local`, `--remote` | switch | `remove` (both sides when neither is given) |
|
|
131
155
|
| `--obsolete`, `--detached` | switch | `remove` |
|
|
156
|
+
| `--force` | switch | `setup`: replace an existing hook |
|
|
157
|
+
| `--no-commit` | switch | `changelog`: update the file without committing |
|
|
132
158
|
| `-days <n>` | value | `remove --obsolete` |
|
|
133
159
|
| `--tags`, `--commits`, `--graph`, `--stashes`, `--pockets` | switch | `list` (also `show`) |
|
|
134
160
|
| `--verbose` | switch | every command |
|
|
135
161
|
| `--dry-run` | switch | every command |
|
|
136
162
|
|
|
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`
|
|
163
|
+
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
164
|
|
|
139
165
|
`--verbose` and `--dry-run` can go anywhere on the line:
|
|
140
166
|
- `--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.
|
|
@@ -146,17 +172,18 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
146
172
|
- **`switch -commit <sha>`** detaches HEAD at that commit so you can look at old code. It refuses a commit that does not exist, and it refuses while tracked files have changes (untracked files do not block it). It lists the files that block it, and points to `gith stash`. On a detached HEAD, `gith switch -b <name>` creates the branch at the current commit, so commits made there stay on it. It does not create or switch branches for other commits, so commits made on a detached HEAD are easy to lose: name a branch before you leave.
|
|
147
173
|
- **`remove -b <branch>`** removes the branch on both sides: the local branch first (with `git branch -d`, so git refuses to delete an unmerged branch), then the origin branch. A side that does not exist is skipped, so a branch that lives only here is still removed. A name that exists in neither place is an error. `--local` touches only this clone; `--remote` touches only origin. Containers (`develop`, `main`, `release/*`, `hotfix/*`) are refused.
|
|
148
174
|
- **`remove`** with no name opens the list of branches and asks which to remove, the same as `remove --branch`. By default it lists branch pairs, meaning branches that exist both here and on origin. `--local` lists local branches and `--remote` lists origin branches. Each choice is removed on the same sides as above. `-limit <n>` shows only the first `n` (all by default), and the picker numbers only those. Pick with the arrow keys: up and down move, space checks an item, enter confirms the checked items, and esc cancels. `switch` takes one item with enter. The list needs a terminal, and that is checked before anything is listed. The checked-out branch and containers are never listed. Choosing items is the confirmation, so nothing else is asked. If a delete fails (for example, an unmerged branch), the removal stops there and earlier picks stay deleted.
|
|
149
|
-
- **`status`** prints the branch and how far it is ahead of or behind its
|
|
175
|
+
- **`status`** prints the current branch and how far it is ahead of or behind its upstream, then counts staged, unstaged, untracked and conflicting files. It also lists all local branches (`*` marks the current branch; `+` marks branches in other worktrees) and remote-tracking branches from every configured remote. Remote branches reflect the last fetch; run `git fetch --all --prune` to refresh them. Empty lists are shown explicitly. Status changes nothing and does not contact remotes.
|
|
150
176
|
- **`update`** fetches `origin/develop` and merges it into the current work branch. It needs a clean working tree and refuses containers. If the merge conflicts, it aborts the merge and says so. It does not push.
|
|
151
177
|
- **`undo`** runs `git reset --soft HEAD~1`: the last commit is removed and its changes stay staged. It refuses when HEAD is already on origin, because undoing a pushed commit needs a force-push.
|
|
152
|
-
- **`
|
|
178
|
+
- **`push` / `pull`** work with the current branch and its same-named branch on origin. They report fresh ahead/behind counts and stop with instructions when histories diverge. Push sets upstream tracking and publishes missing branches; it never force-pushes. Pull requires clean tracked files, fast-forwards only, and points to `gith push` when local commits remain. `--all-branches` applies to every local branch, preflighting all before updating any. Push sends them atomically (origin must support atomic pushes); pull requires each to exist on origin and updates branches without switching. Branches checked out in other worktrees cannot be updated by pull. Remote-only branches are not created. Pull may leave earlier branches updated if a later Git operation fails; it never rolls back successful fast-forwards. Dry runs print actions without fetching or changing refs.
|
|
179
|
+
- **`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
180
|
- **`stash`** saves changes with `git stash push --include-untracked`. `-n` names the stash. It refuses when there is nothing to save.
|
|
154
181
|
- **`find`** (also `search`) lists the newest commits on this branch that match every filter given:
|
|
155
182
|
- `-commit <text>` matches the commit message, ignoring case, and the text is literal.
|
|
156
183
|
- `-file <pattern>` matches the paths changed by the commit, using git's pattern rules (`*api.ts` matches `src/api.ts`). Each commit lists the matching files it changed.
|
|
157
184
|
- `-change <text>` matches commits whose diff adds or removes the text (`git log -S`). With `--regex`, it matches a regular expression against the diff (`git log -G`).
|
|
158
185
|
- `-limit <n>` sets how many to show (default 5). The filters combine with AND.
|
|
159
|
-
- **`show`** is a synonym for `list`: `gith show --tags` and `gith list --tags` do the same. `--pockets` is a fun name for `--stashes`, and it prints
|
|
186
|
+
- **`show`** is a synonym for `list`: `gith show --tags` and `gith list --tags` do the same. `--pockets` is a fun name for `--stashes`, and it prints the gith mascot above the list, matching `gith --version` and the SVG/PNG logo in `assets/`.
|
|
160
187
|
- **`list --stashes`** shows the stashes, newest first, with their date and description. Like the other listings, it shows 5 by default, and `-limit` changes that.
|
|
161
188
|
- **`squash -limit <n>`** combines the last `n` commits on the current work branch into one, using `reset --soft` and a single commit. Without `-m` the result keeps the oldest of the combined commits, its message and author. It refuses when `n` is below 2, when `n` would reach the first commit, when a merge is among them, when any of them is already on origin, on a container, or with a dirty working tree.
|
|
162
189
|
- **`stat`** reads the history of the current branch and prints a table of facts: commit count, top author, the biggest commit (lines added and removed), average size, the most changed file, the busiest day, weekday and hour, the longest streak of days with a commit, night owl and weekend commits, and the conventional commit types. The period is one switch: `--overall` (the default), `--current-year`, `--current-month`, `--current-week` (weeks start on Monday), or the matching `--last-` switch for the previous year, month or week. `-limit <n>` looks only at the last `n` commits. Stat changes nothing.
|
|
@@ -166,12 +193,13 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
166
193
|
- **`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
194
|
- **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
195
|
- **`open`** opens origin in the browser, turning an `ssh` or `https` remote into a web address. GitHub and GitLab style addresses work.
|
|
196
|
+
- **`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
197
|
- **`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
198
|
- **`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
199
|
- **`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
200
|
- **`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
201
|
- **`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
|
|
202
|
+
- **`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
203
|
- **`clean`** runs `git clean -fd`. It permanently removes untracked files and directories, but keeps ignored files.
|
|
176
204
|
- **`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
205
|
|
|
@@ -179,6 +207,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
179
207
|
|
|
180
208
|
- **`create -b`** creates `develop--<name>` from `origin/develop`, pushes it with upstream tracking, and leaves the current branch checked out. The prefix shows the source branch; `create -b develop` and `create -b main` are refused. It does not touch the working tree, so it works with uncommitted changes.
|
|
181
209
|
- **`create -r` / `create -h`** creates a container from its base (`develop` for release, `main` for hotfix), refusing names that already exist on origin. Like `create -b`, it does not switch.
|
|
210
|
+
- **`create -r/-h/--release/--hotfix --update-version --npm`** also sets npm's version to the branch version and commits `package.json` and any npm lockfiles on that new branch before pushing. Both flags are required together and are rejected for work branches. This uses a temporary worktree, leaves your checked-out branch and local changes intact, and creates no version tag. npm lifecycle scripts are skipped; changelog preparation remains part of finishing the release. An already matching package version needs no commit. Without both flags, creation does not run npm or change package versions. The selected base must contain a `package.json` with a version. If npm or the commit fails, the new branch and temporary worktree remain available for inspection and are not pushed.
|
|
182
211
|
- **With `--release` or `--hotfix` (no value)**, the version is worked out from the highest `vX.Y.Z` tag across local tags and origin: `-r` takes the next minor (`1.2.1` → `1.3.0`), `-h` takes the next patch (`1.2.1` → `1.2.2`). With no tags yet, the first release is `0.1.0` and the first hotfix `0.0.1`. The version is printed before the branch is created.
|
|
183
212
|
- **`merge --current-release`** and **`--current-hotfix`** pick the newest `release/*` or `hotfix/*` branch by version (numbers compare as numbers, so `1.10.0` beats `1.9.0`), checking both local branches and origin, and merge into it. The target must exist on origin, because the merge pushes it; a newest branch that only exists here stops the command with a message.
|
|
184
213
|
- **`finish --current-release`** (and `--current-hotfix`) releases the newest `release/*` (or `hotfix/*`), named by its version X.Y.Z. It makes `package.json`'s version match the name (committed as `X.Y.Z`, only when they differ), adds the changelog entry as its own commit (when it is missing), merges the branch into `main` with `--no-ff`, tags `vX.Y.Z` and pushes, merges `main` into `develop` when `develop` exists on origin, and then asks before deleting the branch. A conflict aborts the merge and stops. Projects without a `package.json` keep their version by hand.
|
|
@@ -191,10 +220,14 @@ Containers (`develop`, `main`, `release/*`, `hotfix/*`) can't be merged from, an
|
|
|
191
220
|
|
|
192
221
|
Run as `gith release --patch`, `--minor` or `--major`.
|
|
193
222
|
|
|
194
|
-
- **`patch`** runs from `main`:
|
|
195
|
-
- **`minor` and `major`** run from `develop`:
|
|
223
|
+
- **`patch`** runs from `main`: pushes with tags, then rebases `develop` onto `main`. The push back to `develop` uses `--force-with-lease`, so it refuses to overwrite a `develop` that moved on origin.
|
|
224
|
+
- **`minor` and `major`** run from `develop`: push with tags, merge `develop` into `main`, then bring `main` back to `develop`.
|
|
225
|
+
|
|
226
|
+
Both need a clean working tree. By default, they perform only the Git flow: no package manager runs, no package files change, and no new version tag is created. Existing annotated tags reachable from pushed commits can still be pushed.
|
|
227
|
+
|
|
228
|
+
Add **both `--update-version --npm`** to run `npm version` with the selected patch, minor, or major bump before pushing. This requires npm and a `package.json` with a version; npm lifecycle scripts run as usual. Either flag alone is an error. npm is currently the only supported versioning tool. This repository's `release:patch`, `release:minor`, and `release:major` scripts pass both flags explicitly.
|
|
196
229
|
|
|
197
|
-
|
|
230
|
+
Pushing a `v*` tag publishes the package and creates a GitHub Release with generated notes.
|
|
198
231
|
|
|
199
232
|
## Errors
|
|
200
233
|
|
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"],
|
|
@@ -19,6 +19,8 @@ var SPECS = new Map([
|
|
|
19
19
|
["patch", "switch"],
|
|
20
20
|
["minor", "switch"],
|
|
21
21
|
["major", "switch"],
|
|
22
|
+
["update-version", "switch"],
|
|
23
|
+
["npm", "switch"],
|
|
22
24
|
["remote", "switch"],
|
|
23
25
|
["local", "switch"],
|
|
24
26
|
["tags", "switch"],
|
|
@@ -33,7 +35,10 @@ var SPECS = new Map([
|
|
|
33
35
|
["pockets", "switch"],
|
|
34
36
|
["detached", "switch"],
|
|
35
37
|
["all", "switch"],
|
|
38
|
+
["&push", "switch"],
|
|
39
|
+
["all-branches", "switch"],
|
|
36
40
|
["no-commit", "switch"],
|
|
41
|
+
["force", "switch"],
|
|
37
42
|
["overall", "switch"],
|
|
38
43
|
["current-year", "switch"],
|
|
39
44
|
["current-month", "switch"],
|
|
@@ -57,8 +62,13 @@ var SHORT = {
|
|
|
57
62
|
};
|
|
58
63
|
|
|
59
64
|
// src/components/args/args.utils.ts
|
|
65
|
+
var assertAllowedFlags = (flags, allowed, command) => {
|
|
66
|
+
const extra = [...flags.keys()].find((key) => !allowed.includes(key));
|
|
67
|
+
if (extra !== undefined)
|
|
68
|
+
throw new Error(`${spelling(extra)} is not supported by gith ${command}.`);
|
|
69
|
+
};
|
|
60
70
|
function resolveFlag(token) {
|
|
61
|
-
const match = /^(--?)([a-z][a-z-]
|
|
71
|
+
const match = /^(--?)([a-z][a-z-]*|&push)$/.exec(token);
|
|
62
72
|
if (!match)
|
|
63
73
|
return;
|
|
64
74
|
const word = match[2] ?? "";
|
|
@@ -93,8 +103,6 @@ function parseFlags(argv) {
|
|
|
93
103
|
if (hasNext) {
|
|
94
104
|
flags.set(name, next);
|
|
95
105
|
i++;
|
|
96
|
-
} else if (kind === "optional") {
|
|
97
|
-
flags.set(name, "");
|
|
98
106
|
} else {
|
|
99
107
|
throw new Error(`${token} needs a value.`);
|
|
100
108
|
}
|
|
@@ -166,7 +174,7 @@ function capture(args) {
|
|
|
166
174
|
return run(args.split(" "));
|
|
167
175
|
}
|
|
168
176
|
function hasOnOrigin(branch) {
|
|
169
|
-
return
|
|
177
|
+
return captureArgs(["ls-remote", "--heads", "origin", `refs/heads/${branch}`]) !== "";
|
|
170
178
|
}
|
|
171
179
|
function isAncestor(ancestor, descendant) {
|
|
172
180
|
try {
|
|
@@ -443,7 +451,7 @@ function find(flags) {
|
|
|
443
451
|
|
|
444
452
|
// src/components/flow/flow.ts
|
|
445
453
|
import { execSync } from "node:child_process";
|
|
446
|
-
import { existsSync as
|
|
454
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
447
455
|
|
|
448
456
|
// src/components/prompt/prompt.ts
|
|
449
457
|
import { emitKeypressEvents } from "node:readline";
|
|
@@ -488,20 +496,6 @@ async function confirm(question) {
|
|
|
488
496
|
rl.close();
|
|
489
497
|
}
|
|
490
498
|
}
|
|
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
499
|
async function select(question, options, multi) {
|
|
506
500
|
if (!process.stdin.isTTY)
|
|
507
501
|
throw new Error("this needs a terminal to pick.");
|
|
@@ -612,6 +606,16 @@ function nextVersion(tags, bump) {
|
|
|
612
606
|
}
|
|
613
607
|
|
|
614
608
|
// src/components/flow/flow.utils.ts
|
|
609
|
+
import { spawnSync } from "node:child_process";
|
|
610
|
+
import {
|
|
611
|
+
existsSync as existsSync2,
|
|
612
|
+
mkdtempSync,
|
|
613
|
+
realpathSync,
|
|
614
|
+
symlinkSync,
|
|
615
|
+
unlinkSync
|
|
616
|
+
} from "node:fs";
|
|
617
|
+
import { tmpdir } from "node:os";
|
|
618
|
+
import path2 from "node:path";
|
|
615
619
|
function containers(kind) {
|
|
616
620
|
const prefix = `${kind}/`;
|
|
617
621
|
const local = captureArgs([
|
|
@@ -648,31 +652,86 @@ function nextContainerVersion(kind) {
|
|
|
648
652
|
console.log(`Next ${kind} version from existing tags: ${version}`);
|
|
649
653
|
return version;
|
|
650
654
|
}
|
|
651
|
-
|
|
655
|
+
var open = (kind, version, shouldUpdateVersion) => {
|
|
656
|
+
if (shouldUpdateVersion && !/^\d+\.\d+\.\d+$/.test(version))
|
|
657
|
+
throw new Error("npm version updates require a version in X.Y.Z format; use -r or -h with that version.");
|
|
652
658
|
const name = containerName(kind, version);
|
|
659
|
+
assertBranchName(name);
|
|
660
|
+
if (hasLocal(name))
|
|
661
|
+
throw new Error(`'${name}' already exists locally.`);
|
|
653
662
|
if (hasOnOrigin(name))
|
|
654
663
|
throw new Error(`'${name}' already exists on origin.`);
|
|
655
664
|
const base = containerBase(kind);
|
|
656
665
|
gitArgs(["fetch", "origin", base]);
|
|
666
|
+
const needsVersionUpdate = shouldUpdateVersion && parsePackageVersion(captureArgs(["show", `origin/${base}:package.json`])) !== version;
|
|
657
667
|
gitArgs(["branch", name, `origin/${base}`]);
|
|
668
|
+
if (needsVersionUpdate)
|
|
669
|
+
updateContainerVersion(name, version);
|
|
658
670
|
gitArgs(["push", "-u", "origin", name]);
|
|
659
|
-
}
|
|
671
|
+
};
|
|
672
|
+
var updateContainerVersion = (name, version) => {
|
|
673
|
+
if (isDryRun()) {
|
|
674
|
+
process.stdout.write(`> npm version ${version} --no-git-tag-version --ignore-scripts (temporary worktree for ${name})
|
|
675
|
+
`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const cwd = mkdtempSync(path2.join(tmpdir(), "gith-version-"));
|
|
679
|
+
gitArgs(["worktree", "add", cwd, name]);
|
|
680
|
+
const hasDependencies = existsSync2("node_modules");
|
|
681
|
+
if (hasDependencies)
|
|
682
|
+
symlinkSync(realpathSync("node_modules"), path2.join(cwd, "node_modules"), "junction");
|
|
683
|
+
try {
|
|
684
|
+
commitContainerVersion(cwd, version);
|
|
685
|
+
} finally {
|
|
686
|
+
if (hasDependencies)
|
|
687
|
+
unlinkSync(path2.join(cwd, "node_modules"));
|
|
688
|
+
}
|
|
689
|
+
gitArgs(["worktree", "remove", cwd]);
|
|
690
|
+
};
|
|
691
|
+
var commitContainerVersion = (cwd, version) => {
|
|
692
|
+
const result = spawnSync("npm", ["version", version, "--no-git-tag-version", "--ignore-scripts"], {
|
|
693
|
+
cwd,
|
|
694
|
+
stdio: "inherit",
|
|
695
|
+
shell: process.platform === "win32"
|
|
696
|
+
});
|
|
697
|
+
if (result.error || result.status !== 0)
|
|
698
|
+
throw new Error(`npm version failed; inspect the preserved worktree at ${cwd} and retry.`, { cause: result.error });
|
|
699
|
+
gitArgs([
|
|
700
|
+
"add",
|
|
701
|
+
"--",
|
|
702
|
+
...["package.json", "package-lock.json", "npm-shrinkwrap.json"].filter((file) => existsSync2(path2.join(cwd, file)))
|
|
703
|
+
], { cwd });
|
|
704
|
+
gitArgs(["commit", "-m", version], { cwd });
|
|
705
|
+
};
|
|
660
706
|
|
|
661
707
|
// src/components/flow/flow.ts
|
|
662
|
-
|
|
708
|
+
var create = (flags) => {
|
|
709
|
+
assertAllowedFlags(flags, ["branch", "release", "hotfix", "update-version", "npm"], "create");
|
|
710
|
+
if (flags.has("update-version") !== flags.has("npm"))
|
|
711
|
+
throw new Error("Version updates require both --update-version and --npm; pass both flags or omit both.");
|
|
663
712
|
const kind = onlyOne(flags, ["branch", "release", "hotfix"]);
|
|
664
713
|
if (!kind)
|
|
665
|
-
throw new Error("usage: gith create -b <branch> | -r
|
|
714
|
+
throw new Error("usage: gith create -b <branch> | (-r <version> | -h <version> | --release | --hotfix) [--update-version --npm]");
|
|
666
715
|
const value = flags.get(kind) ?? "";
|
|
667
716
|
if (kind === "branch") {
|
|
717
|
+
if (flags.has("update-version"))
|
|
718
|
+
throw new Error("--update-version --npm only applies to release or hotfix branch creation.");
|
|
668
719
|
if (!value)
|
|
669
720
|
throw new Error("usage: gith create -b <branch>");
|
|
670
721
|
createBranch(value);
|
|
671
722
|
return;
|
|
672
723
|
}
|
|
673
|
-
open(kind, value || nextContainerVersion(kind));
|
|
674
|
-
}
|
|
724
|
+
open(kind, value || nextContainerVersion(kind), flags.has("update-version"));
|
|
725
|
+
};
|
|
675
726
|
async function merge(flags) {
|
|
727
|
+
assertAllowedFlags(flags, [
|
|
728
|
+
"develop",
|
|
729
|
+
"main",
|
|
730
|
+
"release",
|
|
731
|
+
"hotfix",
|
|
732
|
+
"current-release",
|
|
733
|
+
"current-hotfix"
|
|
734
|
+
], "merge");
|
|
676
735
|
if (flags.size === 0)
|
|
677
736
|
return mergePick();
|
|
678
737
|
const kind = onlyOne(flags, [
|
|
@@ -818,7 +877,7 @@ async function mergePick() {
|
|
|
818
877
|
await mergeInto(target, source);
|
|
819
878
|
}
|
|
820
879
|
function alignVersion(version) {
|
|
821
|
-
if (!
|
|
880
|
+
if (!existsSync3("package.json"))
|
|
822
881
|
return;
|
|
823
882
|
const current = parsePackageVersion(readFileSync2("package.json", "utf8"));
|
|
824
883
|
if (current === version)
|
|
@@ -834,10 +893,10 @@ function alignVersion(version) {
|
|
|
834
893
|
gitArgs(["commit", "-m", version, "--", "package.json"]);
|
|
835
894
|
}
|
|
836
895
|
function addChangelogEntry(version) {
|
|
837
|
-
if (!
|
|
896
|
+
if (!existsSync3("package.json"))
|
|
838
897
|
return;
|
|
839
898
|
const file = "CHANGELOG.md";
|
|
840
|
-
if (
|
|
899
|
+
if (existsSync3(file) && readFileSync2(file, "utf8").includes(`## ${version} - `))
|
|
841
900
|
return;
|
|
842
901
|
changelog(new Map);
|
|
843
902
|
}
|
|
@@ -892,15 +951,109 @@ function assertNotPushed() {
|
|
|
892
951
|
throw new Error("HEAD is already on origin; changing it would need a force-push.");
|
|
893
952
|
}
|
|
894
953
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
if (
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
954
|
+
|
|
955
|
+
// src/components/remote/remote.utils.ts
|
|
956
|
+
var getBranches = (flags, command) => {
|
|
957
|
+
if ([...flags.keys()].some((key) => key !== "all-branches"))
|
|
958
|
+
throw new Error(`usage: gith ${command} [--all-branches]`);
|
|
959
|
+
if (!flags.has("all-branches"))
|
|
960
|
+
return [currentBranch()];
|
|
961
|
+
const branches = captureArgs([
|
|
962
|
+
"for-each-ref",
|
|
963
|
+
"--format=%(refname:strip=2)",
|
|
964
|
+
"refs/heads/"
|
|
965
|
+
]);
|
|
966
|
+
if (branches === "")
|
|
967
|
+
throw new Error("No local branches; create a branch and commit before syncing.");
|
|
968
|
+
return branches.split(`
|
|
969
|
+
`);
|
|
970
|
+
};
|
|
971
|
+
var compareOrigin = (branch) => {
|
|
972
|
+
gitArgs(["fetch", "--no-tags", "origin", `refs/heads/${branch}`]);
|
|
973
|
+
const counts = captureArgs([
|
|
974
|
+
"rev-list",
|
|
975
|
+
"--left-right",
|
|
976
|
+
"--count",
|
|
977
|
+
`refs/heads/${branch}...FETCH_HEAD`
|
|
978
|
+
]);
|
|
979
|
+
if (!/^\d+\s+\d+$/.test(counts))
|
|
980
|
+
throw new Error("Cannot compare commits with origin; check the repository with git fsck.");
|
|
981
|
+
const parts = counts.split(/\s+/);
|
|
982
|
+
const ahead = Number(parts[0]);
|
|
983
|
+
const behind = Number(parts[1]);
|
|
984
|
+
process.stdout.write(`${branch} -> origin/${branch}: ahead ${ahead}, behind ${behind}.
|
|
985
|
+
`);
|
|
986
|
+
return { ahead, behind };
|
|
987
|
+
};
|
|
988
|
+
var updateBranch = (branch, current) => {
|
|
989
|
+
if (branch === current) {
|
|
990
|
+
gitArgs(["fetch", "--no-tags", "origin", `refs/heads/${branch}`]);
|
|
991
|
+
gitArgs(["merge", "--ff-only", "FETCH_HEAD"]);
|
|
992
|
+
} else {
|
|
993
|
+
gitArgs([
|
|
994
|
+
"fetch",
|
|
995
|
+
"--no-tags",
|
|
996
|
+
"origin",
|
|
997
|
+
`refs/heads/${branch}:refs/heads/${branch}`
|
|
998
|
+
]);
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// src/components/remote/remote.ts
|
|
1003
|
+
var push = (flags) => {
|
|
1004
|
+
const branches = getBranches(flags, "push");
|
|
1005
|
+
for (const branch of branches) {
|
|
1006
|
+
if (isDryRun())
|
|
1007
|
+
continue;
|
|
1008
|
+
if (!hasOnOrigin(branch)) {
|
|
1009
|
+
process.stdout.write(`Publishing ${branch} on origin and setting upstream tracking.
|
|
1010
|
+
`);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
const { ahead, behind } = compareOrigin(branch);
|
|
1014
|
+
if (behind > 0)
|
|
1015
|
+
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.`);
|
|
1016
|
+
process.stdout.write(`Pushing ${ahead} commit(s) from ${branch}; ensuring upstream tracking.
|
|
1017
|
+
`);
|
|
1018
|
+
}
|
|
1019
|
+
gitArgs([
|
|
1020
|
+
"push",
|
|
1021
|
+
...flags.has("all-branches") ? ["--atomic"] : [],
|
|
1022
|
+
"-u",
|
|
1023
|
+
"origin",
|
|
1024
|
+
...branches.map((branch) => `refs/heads/${branch}:refs/heads/${branch}`)
|
|
1025
|
+
]);
|
|
1026
|
+
if (!isDryRun())
|
|
1027
|
+
process.stdout.write(`Push complete: ${branches.length} branch(es) synced with origin.
|
|
1028
|
+
`);
|
|
1029
|
+
};
|
|
1030
|
+
var pull = (flags) => {
|
|
1031
|
+
const branches = getBranches(flags, "pull");
|
|
1032
|
+
const current = currentBranch();
|
|
1033
|
+
assertCleanTree();
|
|
1034
|
+
const updates = [];
|
|
1035
|
+
for (const branch of branches) {
|
|
1036
|
+
if (isDryRun())
|
|
1037
|
+
continue;
|
|
1038
|
+
if (!hasOnOrigin(branch))
|
|
1039
|
+
throw new Error(`origin/${branch} does not exist; run gith push to publish this branch first.`);
|
|
1040
|
+
const { ahead, behind } = compareOrigin(branch);
|
|
1041
|
+
if (ahead > 0 && behind > 0)
|
|
1042
|
+
throw new Error(`${branch} has diverged; merge or rebase origin's changes explicitly. No commits were changed.`);
|
|
1043
|
+
if (behind > 0)
|
|
1044
|
+
updates.push({ branch, behind });
|
|
1045
|
+
else
|
|
1046
|
+
process.stdout.write(ahead > 0 ? `${branch}: no incoming commits; run gith push to publish ${ahead} local commit(s).
|
|
1047
|
+
` : `${branch}: already up to date.
|
|
1048
|
+
`);
|
|
1049
|
+
}
|
|
1050
|
+
for (const { branch, behind } of isDryRun() ? branches.map((branch) => ({ branch, behind: 0 })) : updates) {
|
|
1051
|
+
updateBranch(branch, current);
|
|
1052
|
+
if (!isDryRun())
|
|
1053
|
+
process.stdout.write(`Pulled ${behind} commit(s) into ${branch}.
|
|
1054
|
+
`);
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
904
1057
|
|
|
905
1058
|
// src/components/history/history.ts
|
|
906
1059
|
function update(flags) {
|
|
@@ -926,43 +1079,33 @@ function undo(flags) {
|
|
|
926
1079
|
assertNotPushed();
|
|
927
1080
|
gitArgs(["reset", "--soft", "HEAD~1"]);
|
|
928
1081
|
}
|
|
929
|
-
|
|
1082
|
+
var save = (flags) => {
|
|
930
1083
|
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
|
-
|
|
1084
|
+
const extra = [...flags.keys()].filter((key) => key !== "message" && key !== "all" && key !== "&push");
|
|
1085
|
+
if (extra.length > 0 || message === undefined || message.trim() === "")
|
|
1086
|
+
throw new Error('usage: gith save -m <message> [--all] ["--&push"]');
|
|
1087
|
+
currentBranch();
|
|
935
1088
|
const unstaged = captureArgs(["add", "-A", "--dry-run"]).split(`
|
|
936
1089
|
`).filter(Boolean);
|
|
937
|
-
const stageAll = flags.has("all") || unstaged.length === 1
|
|
1090
|
+
const stageAll = flags.has("all") || unstaged.length === 1;
|
|
938
1091
|
if (stageAll && unstaged.length > 0) {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1092
|
+
process.stdout.write(`Staging ${unstaged.length} unstaged change(s):
|
|
1093
|
+
${unstaged.join(`
|
|
1094
|
+
`)}
|
|
1095
|
+
`);
|
|
942
1096
|
}
|
|
943
1097
|
if (stageAll)
|
|
944
1098
|
gitArgs(["add", "-A"]);
|
|
945
|
-
if (captureArgs(["diff", "--cached", "--name-only"]) === "") {
|
|
946
|
-
throw new Error("nothing staged; stage the changes
|
|
1099
|
+
if (captureArgs(["diff", "--cached", "--name-only"]) === "" && !(isDryRun() && stageAll && unstaged.length > 0)) {
|
|
1100
|
+
throw new Error("nothing staged; stage the changes with git add, or use gith save -m <message> --all.");
|
|
947
1101
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const rewrites = amend && remoteSha(branch) === previous;
|
|
956
|
-
const question = rewrites ? `Force-push ${branch} to origin? [Y/n] ` : `Push ${branch} to origin? [Y/n] `;
|
|
957
|
-
if (!await confirm(question)) {
|
|
958
|
-
console.log("Saved locally; not pushed.");
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
if (rewrites)
|
|
962
|
-
gitArgs(["push", "--force-with-lease", "origin", branch]);
|
|
963
|
-
else
|
|
964
|
-
gitArgs(["push", "-u", "origin", branch]);
|
|
965
|
-
}
|
|
1102
|
+
gitArgs(["commit", "-m", message]);
|
|
1103
|
+
process.stdout.write(isDryRun() ? `Would save locally.
|
|
1104
|
+
` : `Saved locally.
|
|
1105
|
+
`);
|
|
1106
|
+
if (flags.has("&push"))
|
|
1107
|
+
push(new Map);
|
|
1108
|
+
};
|
|
966
1109
|
async function squash(flags) {
|
|
967
1110
|
const count = parseLimit(flags);
|
|
968
1111
|
const message = flags.get("message");
|
|
@@ -1004,37 +1147,70 @@ async function squash(flags) {
|
|
|
1004
1147
|
else
|
|
1005
1148
|
gitArgs(["commit", "-C", oldest]);
|
|
1006
1149
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1150
|
+
|
|
1151
|
+
// src/components/version/version.constants.ts
|
|
1152
|
+
var MASCOT = [
|
|
1153
|
+
" o",
|
|
1154
|
+
" o | o",
|
|
1155
|
+
" \\ | /",
|
|
1156
|
+
" \\ | /",
|
|
1157
|
+
" \\ | /",
|
|
1158
|
+
" \\|/",
|
|
1159
|
+
" |",
|
|
1160
|
+
" .---.",
|
|
1161
|
+
" ( o o )",
|
|
1162
|
+
" '---'",
|
|
1163
|
+
" |",
|
|
1164
|
+
" /|\\",
|
|
1165
|
+
" / \\"
|
|
1166
|
+
];
|
|
1015
1167
|
|
|
1016
1168
|
// src/components/list/list.constants.ts
|
|
1017
1169
|
var DEFAULT_LIMIT2 = 5;
|
|
1018
|
-
var
|
|
1019
|
-
|
|
1020
|
-
"
|
|
1021
|
-
"
|
|
1022
|
-
"
|
|
1023
|
-
"
|
|
1170
|
+
var GRAPH_ARGS = [
|
|
1171
|
+
"-c",
|
|
1172
|
+
"log.graphColors=cyan,magenta,blue,yellow,green,red",
|
|
1173
|
+
"log",
|
|
1174
|
+
"--graph",
|
|
1175
|
+
"--topo-order",
|
|
1176
|
+
"--decorate=short",
|
|
1177
|
+
"--date=format:%Y-%m-%d %H:%M",
|
|
1178
|
+
"--format=%C(bold yellow)%h%C(reset) %C(bold)%s%C(reset) %C(auto)%d%C(reset)%n%C(dim) └─ %an · %ad%C(reset)%n"
|
|
1024
1179
|
];
|
|
1025
1180
|
|
|
1026
1181
|
// src/components/list/list.utils.ts
|
|
1027
1182
|
function yesNo(value) {
|
|
1028
1183
|
return value ? "yes" : "no";
|
|
1029
1184
|
}
|
|
1030
|
-
|
|
1185
|
+
var commits = (limit, graph) => {
|
|
1031
1186
|
if (!hasHead()) {
|
|
1032
|
-
|
|
1187
|
+
process.stdout.write(`No commits.
|
|
1188
|
+
`);
|
|
1033
1189
|
return;
|
|
1034
1190
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1191
|
+
if (!graph) {
|
|
1192
|
+
process.stdout.write(`${captureArgs(["log", `-n${limit}`, "--date=short", "--format=%h %ad %an %s"])}
|
|
1193
|
+
`);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const { NO_COLOR: noColor, TERM: terminal } = process.env;
|
|
1197
|
+
const color = process.stdout.isTTY && noColor === undefined && terminal !== "dumb";
|
|
1198
|
+
const branch = captureArgs(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
1199
|
+
process.stdout.write(`
|
|
1200
|
+
╭─ COMMIT GRAPH ─────────────────────────────────────────
|
|
1201
|
+
│ ${branch} · branch history · limit ${limit}
|
|
1202
|
+
│
|
|
1203
|
+
`);
|
|
1204
|
+
process.stdout.write(`${captureArgs([
|
|
1205
|
+
...GRAPH_ARGS,
|
|
1206
|
+
`-n${limit}`,
|
|
1207
|
+
color ? "--color=always" : "--color=never"
|
|
1208
|
+
])}
|
|
1209
|
+
`);
|
|
1210
|
+
process.stdout.write(`╰─ * commit · branches & tags in parentheses · -l <n> shows more
|
|
1211
|
+
|
|
1212
|
+
`);
|
|
1213
|
+
};
|
|
1038
1214
|
function hasHead() {
|
|
1039
1215
|
try {
|
|
1040
1216
|
captureArgs(["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
@@ -1064,6 +1240,7 @@ function tagPairs(local, origin) {
|
|
|
1064
1240
|
});
|
|
1065
1241
|
}
|
|
1066
1242
|
function list(flags) {
|
|
1243
|
+
assertAllowedFlags(flags, ["tags", "commits", "stashes", "pockets", "graph", "limit"], "list");
|
|
1067
1244
|
const kind = onlyOne(flags, ["tags", "commits", "stashes", "pockets"]);
|
|
1068
1245
|
if (!kind)
|
|
1069
1246
|
throw new Error("usage: gith list --tags | --commits [--graph] | --stashes | --pockets [-limit <n>]");
|
|
@@ -1083,10 +1260,12 @@ function localTags() {
|
|
|
1083
1260
|
return captureArgs(["tag", "--list"]).split(`
|
|
1084
1261
|
`).filter(Boolean);
|
|
1085
1262
|
}
|
|
1086
|
-
|
|
1087
|
-
if (
|
|
1088
|
-
|
|
1089
|
-
`)
|
|
1263
|
+
var stashes = (limit, withMascot) => {
|
|
1264
|
+
if (withMascot)
|
|
1265
|
+
process.stdout.write(`${MASCOT.join(`
|
|
1266
|
+
`)}
|
|
1267
|
+
my precious pockets!
|
|
1268
|
+
`);
|
|
1090
1269
|
const rows = captureArgs([
|
|
1091
1270
|
"stash",
|
|
1092
1271
|
"list",
|
|
@@ -1095,7 +1274,9 @@ function stashes(limit, withGorlum) {
|
|
|
1095
1274
|
]).split(`
|
|
1096
1275
|
`).filter(Boolean);
|
|
1097
1276
|
if (rows.length === 0) {
|
|
1098
|
-
|
|
1277
|
+
process.stdout.write(withMascot ? `There is nothing in my pockets!
|
|
1278
|
+
` : `No stashes.
|
|
1279
|
+
`);
|
|
1099
1280
|
return;
|
|
1100
1281
|
}
|
|
1101
1282
|
const shown = rows.slice(0, limit);
|
|
@@ -1104,7 +1285,7 @@ function stashes(limit, withGorlum) {
|
|
|
1104
1285
|
if (shown.length < rows.length) {
|
|
1105
1286
|
console.log(`showing ${shown.length} of ${rows.length}; -limit <n> shows more`);
|
|
1106
1287
|
}
|
|
1107
|
-
}
|
|
1288
|
+
};
|
|
1108
1289
|
function tags(limit) {
|
|
1109
1290
|
const pairs = tagPairs(localTags(), originTags());
|
|
1110
1291
|
if (pairs.length === 0) {
|
|
@@ -1196,6 +1377,7 @@ async function switchBranch(flags) {
|
|
|
1196
1377
|
switchToCommit(commit);
|
|
1197
1378
|
return;
|
|
1198
1379
|
}
|
|
1380
|
+
assertAllowedFlags(flags, ["branch"], "switch");
|
|
1199
1381
|
const branch = requireBranch(flags, "switch");
|
|
1200
1382
|
const from = isDetached() ? "HEAD" : undefined;
|
|
1201
1383
|
const exists = (name) => hasLocal(name) || hasOnOrigin(name);
|
|
@@ -1262,6 +1444,16 @@ function detachedOptions(times, current) {
|
|
|
1262
1444
|
return options.sort((x, y) => x.name.localeCompare(y.name));
|
|
1263
1445
|
}
|
|
1264
1446
|
async function remove(flags) {
|
|
1447
|
+
assertAllowedFlags(flags, [
|
|
1448
|
+
"branch",
|
|
1449
|
+
"local",
|
|
1450
|
+
"remote",
|
|
1451
|
+
"obsolete",
|
|
1452
|
+
"detached",
|
|
1453
|
+
"days",
|
|
1454
|
+
"all",
|
|
1455
|
+
"limit"
|
|
1456
|
+
], "remove");
|
|
1265
1457
|
const name = flags.get("branch") ?? "";
|
|
1266
1458
|
const side = onlyOne(flags, ["local", "remote"]);
|
|
1267
1459
|
const scope = side ?? "pair";
|
|
@@ -1571,23 +1763,26 @@ function npmVersion(kind) {
|
|
|
1571
1763
|
}
|
|
1572
1764
|
|
|
1573
1765
|
// src/components/release/release.ts
|
|
1574
|
-
|
|
1766
|
+
var release = (kind, flags) => {
|
|
1575
1767
|
if (!kind || !KINDS.includes(kind))
|
|
1576
|
-
throw new Error("usage: gith release
|
|
1768
|
+
throw new Error("usage: gith release --patch | --minor | --major [--update-version --npm]");
|
|
1769
|
+
if (flags.has("update-version") !== flags.has("npm"))
|
|
1770
|
+
throw new Error("Version updates require both --update-version and --npm; pass both flags or omit both.");
|
|
1577
1771
|
assertCleanTree();
|
|
1578
1772
|
if (kind === "patch") {
|
|
1579
|
-
releasePatch();
|
|
1773
|
+
releasePatch(flags.has("update-version"));
|
|
1580
1774
|
} else {
|
|
1581
|
-
releaseFromDevelop(kind);
|
|
1775
|
+
releaseFromDevelop(kind, flags.has("update-version"));
|
|
1582
1776
|
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1777
|
+
};
|
|
1778
|
+
var releasePatch = (shouldUpdateVersion) => {
|
|
1585
1779
|
console.log(`
|
|
1586
1780
|
\uD83D\uDCCB Starting patch release from main...
|
|
1587
1781
|
`);
|
|
1588
1782
|
git("checkout main");
|
|
1589
1783
|
git("pull");
|
|
1590
|
-
|
|
1784
|
+
if (shouldUpdateVersion)
|
|
1785
|
+
npmVersion("patch");
|
|
1591
1786
|
git("push --follow-tags");
|
|
1592
1787
|
git("checkout develop");
|
|
1593
1788
|
git("pull");
|
|
@@ -1598,8 +1793,8 @@ function releasePatch() {
|
|
|
1598
1793
|
console.log(`
|
|
1599
1794
|
✅ Patch release complete!
|
|
1600
1795
|
`);
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1796
|
+
};
|
|
1797
|
+
var releaseFromDevelop = (kind, shouldUpdateVersion) => {
|
|
1603
1798
|
console.log(`
|
|
1604
1799
|
\uD83D\uDCCB Starting ${kind} release from develop...
|
|
1605
1800
|
`);
|
|
@@ -1607,7 +1802,8 @@ function releaseFromDevelop(kind) {
|
|
|
1607
1802
|
if (current !== "develop")
|
|
1608
1803
|
throw new Error(`must be on 'develop', currently on '${current}'.`);
|
|
1609
1804
|
git("pull");
|
|
1610
|
-
|
|
1805
|
+
if (shouldUpdateVersion)
|
|
1806
|
+
npmVersion(kind);
|
|
1611
1807
|
git("push --follow-tags");
|
|
1612
1808
|
git("checkout main");
|
|
1613
1809
|
git("pull");
|
|
@@ -1615,10 +1811,120 @@ function releaseFromDevelop(kind) {
|
|
|
1615
1811
|
git("push");
|
|
1616
1812
|
git("checkout develop");
|
|
1617
1813
|
git("pull origin main");
|
|
1814
|
+
git("push");
|
|
1618
1815
|
assertSynced(["main", "develop"]);
|
|
1619
1816
|
console.log(`
|
|
1620
1817
|
✅ ${kind} release complete!
|
|
1621
1818
|
`);
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
// src/components/setup/setup.ts
|
|
1822
|
+
import {
|
|
1823
|
+
chmodSync,
|
|
1824
|
+
existsSync as existsSync5,
|
|
1825
|
+
mkdirSync,
|
|
1826
|
+
readFileSync as readFileSync3,
|
|
1827
|
+
writeFileSync as writeFileSync2
|
|
1828
|
+
} from "node:fs";
|
|
1829
|
+
import path4 from "node:path";
|
|
1830
|
+
|
|
1831
|
+
// src/components/setup/setup.constants.ts
|
|
1832
|
+
var HOOKS_DIR = ".githooks";
|
|
1833
|
+
var HOOK_FILE = `${HOOKS_DIR}/pre-commit`;
|
|
1834
|
+
var HOOKS_PATH_COMMAND = `git config core.hooksPath ${HOOKS_DIR}`;
|
|
1835
|
+
var LOCKFILES = [
|
|
1836
|
+
["bun.lock", "bun"],
|
|
1837
|
+
["bun.lockb", "bun"],
|
|
1838
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
1839
|
+
["yarn.lock", "yarn"],
|
|
1840
|
+
["package-lock.json", "npm"]
|
|
1841
|
+
];
|
|
1842
|
+
var RUN_PREFIX = {
|
|
1843
|
+
bun: "bun run",
|
|
1844
|
+
pnpm: "pnpm run",
|
|
1845
|
+
yarn: "yarn",
|
|
1846
|
+
npm: "npm run"
|
|
1847
|
+
};
|
|
1848
|
+
|
|
1849
|
+
// src/components/setup/setup.utils.ts
|
|
1850
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1851
|
+
import path3 from "node:path";
|
|
1852
|
+
function checkBlock(manager, script, label) {
|
|
1853
|
+
return `echo "▶ ${label}"
|
|
1854
|
+
${RUN_PREFIX[manager]} ${script}
|
|
1855
|
+
|
|
1856
|
+
`;
|
|
1857
|
+
}
|
|
1858
|
+
function buildPreCommitHook(pkg, manager, isTypeScript) {
|
|
1859
|
+
const checks = [
|
|
1860
|
+
isTypeScript || pkg.scripts?.lint ? checkBlock(manager, "lint", "Lint") : "",
|
|
1861
|
+
isTypeScript || pkg.scripts?.typecheck ? checkBlock(manager, "typecheck", "Typecheck") : "",
|
|
1862
|
+
pkg.scripts?.test ? checkBlock(manager, "test", "Test") : ""
|
|
1863
|
+
].join("");
|
|
1864
|
+
return `#!/usr/bin/env sh
|
|
1865
|
+
# Generated by gith setup -n npm - runs on every commit via core.hooksPath
|
|
1866
|
+
set -e
|
|
1867
|
+
|
|
1868
|
+
${checks}`;
|
|
1869
|
+
}
|
|
1870
|
+
function detectPackageManager(projectPath) {
|
|
1871
|
+
const lockfile = LOCKFILES.find(([file]) => existsSync4(path3.join(projectPath, file)));
|
|
1872
|
+
return lockfile?.[1] ?? "npm";
|
|
1873
|
+
}
|
|
1874
|
+
function isTypeScriptProject(projectPath, pkg) {
|
|
1875
|
+
if (existsSync4(path3.join(projectPath, "tsconfig.json")))
|
|
1876
|
+
return true;
|
|
1877
|
+
return Boolean(pkg.dependencies?.typescript || pkg.devDependencies?.typescript);
|
|
1878
|
+
}
|
|
1879
|
+
function withPrepareScript(pkg) {
|
|
1880
|
+
return { ...pkg, scripts: { ...pkg.scripts, prepare: HOOKS_PATH_COMMAND } };
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/components/setup/setup.ts
|
|
1884
|
+
var SETUP_USAGE = "usage: gith setup -n npm [--force]";
|
|
1885
|
+
function setup(flags) {
|
|
1886
|
+
const unknown = [...flags.keys()].filter((key) => key !== "name" && key !== "force");
|
|
1887
|
+
if (unknown.length > 0 || flags.get("name") === undefined) {
|
|
1888
|
+
throw new Error(SETUP_USAGE);
|
|
1889
|
+
}
|
|
1890
|
+
const target = flags.get("name");
|
|
1891
|
+
if (target !== "npm") {
|
|
1892
|
+
throw new Error(`unknown setup '${target}': only npm is supported. ${SETUP_USAGE}`);
|
|
1893
|
+
}
|
|
1894
|
+
setupNpmHook({ force: flags.has("force") });
|
|
1895
|
+
}
|
|
1896
|
+
function setupNpmHook({ force }) {
|
|
1897
|
+
const root = process.cwd();
|
|
1898
|
+
if (!existsSync5(path4.join(root, ".git"))) {
|
|
1899
|
+
throw new Error("not a git repository root: run it in the folder that holds .git");
|
|
1900
|
+
}
|
|
1901
|
+
const packagePath = path4.join(root, "package.json");
|
|
1902
|
+
if (!existsSync5(packagePath))
|
|
1903
|
+
throw new Error("no package.json in this folder");
|
|
1904
|
+
const hookPath = path4.join(root, HOOK_FILE);
|
|
1905
|
+
if (existsSync5(hookPath) && !force) {
|
|
1906
|
+
throw new Error(`${HOOK_FILE} already exists. Pass --force to overwrite it.`);
|
|
1907
|
+
}
|
|
1908
|
+
const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
|
|
1909
|
+
const manager = detectPackageManager(root);
|
|
1910
|
+
const typescript = isTypeScriptProject(root, pkg);
|
|
1911
|
+
const hook = buildPreCommitHook(pkg, manager, typescript);
|
|
1912
|
+
if (isDryRun()) {
|
|
1913
|
+
console.log(`dry run: would write ${HOOK_FILE} (package manager: ${manager})`);
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
mkdirSync(path4.join(root, HOOKS_DIR), { recursive: true });
|
|
1917
|
+
writeFileSync2(hookPath, hook);
|
|
1918
|
+
try {
|
|
1919
|
+
chmodSync(hookPath, 493);
|
|
1920
|
+
} catch {}
|
|
1921
|
+
gitArgs(["config", "core.hooksPath", HOOKS_DIR]);
|
|
1922
|
+
if (pkg.scripts?.prepare !== HOOKS_PATH_COMMAND) {
|
|
1923
|
+
writeFileSync2(packagePath, `${JSON.stringify(withPrepareScript(pkg), null, 2)}
|
|
1924
|
+
`);
|
|
1925
|
+
console.log(`✏️ Added "prepare" → "${HOOKS_PATH_COMMAND}" to package.json`);
|
|
1926
|
+
}
|
|
1927
|
+
console.log(`✅ Wrote ${HOOK_FILE} and set core.hooksPath (package manager: ${manager}${typescript ? ", TypeScript" : ""})`);
|
|
1622
1928
|
}
|
|
1623
1929
|
|
|
1624
1930
|
// src/components/stash/stash.ts
|
|
@@ -1819,6 +2125,7 @@ function table(rows) {
|
|
|
1819
2125
|
`);
|
|
1820
2126
|
}
|
|
1821
2127
|
function stat(flags) {
|
|
2128
|
+
assertAllowedFlags(flags, [...PERIODS, "limit"], "stat");
|
|
1822
2129
|
const period = onlyOne(flags, PERIODS) ?? "overall";
|
|
1823
2130
|
const limit = parseLimit(flags);
|
|
1824
2131
|
const range = periodRange(period, new Date);
|
|
@@ -1878,19 +2185,32 @@ function parseStatus(output) {
|
|
|
1878
2185
|
}
|
|
1879
2186
|
return status;
|
|
1880
2187
|
}
|
|
1881
|
-
|
|
2188
|
+
var status = (flags) => {
|
|
1882
2189
|
if (flags.size > 0)
|
|
1883
2190
|
throw new Error("usage: gith status");
|
|
1884
2191
|
const info = parseStatus(captureArgs(["status", "--porcelain=v2", "--branch"]));
|
|
1885
2192
|
const against = info.upstream ? `${info.branch} -> ${info.upstream} ahead ${info.ahead}, behind ${info.behind}` : `${info.branch} (no upstream)`;
|
|
1886
|
-
|
|
2193
|
+
process.stdout.write(`${against}
|
|
2194
|
+
`);
|
|
1887
2195
|
const changed = info.staged + info.unstaged + info.untracked + info.conflicts;
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2196
|
+
process.stdout.write(changed === 0 ? `working tree clean
|
|
2197
|
+
` : `staged ${info.staged}, unstaged ${info.unstaged}, untracked ${info.untracked}, conflicts ${info.conflicts}
|
|
2198
|
+
`);
|
|
2199
|
+
for (const [heading, scope] of [
|
|
2200
|
+
["Local branches (* current, + other worktree)", "--list"],
|
|
2201
|
+
["Remote branches (last fetched)", "--remotes"]
|
|
2202
|
+
]) {
|
|
2203
|
+
const branches = captureArgs(["branch", "--no-color", scope]);
|
|
2204
|
+
process.stdout.write(`
|
|
2205
|
+
${heading}:
|
|
2206
|
+
`);
|
|
2207
|
+
process.stdout.write(branches.length === 0 ? ` (none)
|
|
2208
|
+
` : `${branches.split(`
|
|
2209
|
+
`).map((branch) => ` ${branch.trim()}`).join(`
|
|
2210
|
+
`)}
|
|
2211
|
+
`);
|
|
1891
2212
|
}
|
|
1892
|
-
|
|
1893
|
-
}
|
|
2213
|
+
};
|
|
1894
2214
|
|
|
1895
2215
|
// src/components/tree/tree.utils.ts
|
|
1896
2216
|
function flagsOf(worktree) {
|
|
@@ -1958,34 +2278,22 @@ function tree(flags) {
|
|
|
1958
2278
|
}
|
|
1959
2279
|
|
|
1960
2280
|
// src/components/version/version.ts
|
|
1961
|
-
import { readFileSync as
|
|
1962
|
-
|
|
1963
|
-
// src/components/version/version.constants.ts
|
|
1964
|
-
var MASCOT = [
|
|
1965
|
-
" o",
|
|
1966
|
-
" /",
|
|
1967
|
-
" o----o----o",
|
|
1968
|
-
" \\ | /",
|
|
1969
|
-
" \\ | /",
|
|
1970
|
-
" (o_o)",
|
|
1971
|
-
" /|_|\\",
|
|
1972
|
-
" d b"
|
|
1973
|
-
];
|
|
2281
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1974
2282
|
|
|
1975
2283
|
// src/components/version/version.utils.ts
|
|
1976
|
-
import { existsSync as
|
|
1977
|
-
import
|
|
2284
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
2285
|
+
import path5 from "node:path";
|
|
1978
2286
|
import { fileURLToPath } from "node:url";
|
|
1979
2287
|
function packageJsonPath() {
|
|
1980
|
-
let dir =
|
|
2288
|
+
let dir = path5.dirname(fileURLToPath(import.meta.url));
|
|
1981
2289
|
for (;; ) {
|
|
1982
|
-
const candidate =
|
|
1983
|
-
if (
|
|
1984
|
-
const pkg = JSON.parse(
|
|
2290
|
+
const candidate = path5.join(dir, "package.json");
|
|
2291
|
+
if (existsSync6(candidate)) {
|
|
2292
|
+
const pkg = JSON.parse(readFileSync4(candidate, "utf8"));
|
|
1985
2293
|
if (pkg.name === "@tertium/gith")
|
|
1986
2294
|
return candidate;
|
|
1987
2295
|
}
|
|
1988
|
-
const parent =
|
|
2296
|
+
const parent = path5.dirname(dir);
|
|
1989
2297
|
if (parent === dir)
|
|
1990
2298
|
throw new Error("gith package.json not found");
|
|
1991
2299
|
dir = parent;
|
|
@@ -1994,12 +2302,23 @@ function packageJsonPath() {
|
|
|
1994
2302
|
|
|
1995
2303
|
// src/components/version/version.ts
|
|
1996
2304
|
function readVersion() {
|
|
1997
|
-
const pkg = JSON.parse(
|
|
2305
|
+
const pkg = JSON.parse(readFileSync5(packageJsonPath(), "utf8"));
|
|
1998
2306
|
return pkg.version ?? "unknown";
|
|
1999
2307
|
}
|
|
2000
2308
|
function versionText() {
|
|
2001
2309
|
const version = readVersion();
|
|
2002
|
-
const side = [
|
|
2310
|
+
const side = [
|
|
2311
|
+
"",
|
|
2312
|
+
"",
|
|
2313
|
+
"",
|
|
2314
|
+
"",
|
|
2315
|
+
"",
|
|
2316
|
+
"",
|
|
2317
|
+
"",
|
|
2318
|
+
"",
|
|
2319
|
+
`gith v${version}`,
|
|
2320
|
+
"git happens."
|
|
2321
|
+
];
|
|
2003
2322
|
return MASCOT.map((line, i) => `${line.padEnd(16)}${side[i] ?? ""}`.trimEnd()).join(`
|
|
2004
2323
|
`);
|
|
2005
2324
|
}
|
|
@@ -2011,24 +2330,26 @@ Start and finish work
|
|
|
2011
2330
|
gith create -b <branch> work branch develop--<branch> from develop (no switch)
|
|
2012
2331
|
gith create -r <version> release/<version> from develop
|
|
2013
2332
|
gith create -h <version> hotfix/<version> from main
|
|
2014
|
-
gith create --release | --hotfix version from the latest tag
|
|
2333
|
+
gith create --release | --hotfix next version from the latest tag
|
|
2334
|
+
gith create -r <version> --update-version --npm also commit npm version on the new branch; works with -h, --release, --hotfix
|
|
2015
2335
|
gith switch list local and origin branches, and switch to the one picked
|
|
2016
2336
|
gith switch -b <branch> switch to a branch, creating develop--<branch> from develop if missing (on a detached HEAD: <branch> from that commit)
|
|
2017
2337
|
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
|
|
2338
|
+
gith switch --release | --hotfix switch to the newest release/* or hotfix/* (local or origin); -r <v> | -h <v> for one version
|
|
2019
2339
|
gith update merge origin/develop into this work branch
|
|
2020
2340
|
gith finish --current-release | --current-hotfix release the newest release/* or hotfix/*: version, changelog, merge to main, tag, back to develop
|
|
2021
2341
|
gith merge --develop merge this branch into develop
|
|
2022
2342
|
gith merge --main merge this branch into main
|
|
2023
2343
|
gith merge pick the branch to merge, then the target (main, develop, release/*, hotfix/*)
|
|
2024
|
-
gith merge
|
|
2344
|
+
gith merge --release | --hotfix merge into the only matching container, or pick one
|
|
2345
|
+
gith merge -r <v> | -h <v> merge into release/<v> or hotfix/<v>
|
|
2025
2346
|
gith merge --current-release merge this branch into the newest release (local or origin)
|
|
2026
2347
|
gith merge --current-hotfix merge this branch into the newest hotfix (local or origin)
|
|
2027
|
-
gith release --patch | --minor | --major
|
|
2348
|
+
gith release --patch | --minor | --major sync release branches; add --update-version --npm to bump npm version
|
|
2028
2349
|
|
|
2029
2350
|
Fix history
|
|
2030
2351
|
gith undo undo the last commit, keep its changes staged (not if pushed)
|
|
2031
|
-
gith save
|
|
2352
|
+
gith save -m <msg> [--all] ["--&push"] new commit without prompts; push only with "--&push"
|
|
2032
2353
|
gith squash -limit <n> [-m <msg>] combine the last n commits into one (keeps the oldest message)
|
|
2033
2354
|
|
|
2034
2355
|
Save and reset work
|
|
@@ -2051,24 +2372,27 @@ Clean up branches (every removal shows what it will remove first)
|
|
|
2051
2372
|
gith rm alias of remove
|
|
2052
2373
|
|
|
2053
2374
|
Look at the repo
|
|
2054
|
-
gith status
|
|
2055
|
-
gith list --commits [--graph] [-l <n>] newest commits on this branch (default 5)
|
|
2375
|
+
gith status working-tree summary, all local and remote-tracking branches
|
|
2376
|
+
gith list --commits [--graph] [-l <n>] newest commits on this branch; --graph adds colored lanes, refs, author/date (default 5)
|
|
2056
2377
|
gith list --tags [-l <n>] tags with a local and origin column (newest 5 by default)
|
|
2057
|
-
gith list --stashes [-l <n>] stashes, newest first (default 5); --pockets shows them with
|
|
2378
|
+
gith list --stashes [-l <n>] stashes, newest first (default 5); --pockets shows them with the gith mascot
|
|
2058
2379
|
gith show ... same as gith list
|
|
2059
2380
|
gith tree [-l <n>] working trees: path, branch, HEAD (current marked *)
|
|
2060
2381
|
gith find|search [-commit <text>] [-file <pattern>] [-change <text>] [--regex] [-limit <n>]
|
|
2061
2382
|
newest commits on this branch matching every filter (default 5)
|
|
2062
|
-
gith stat [
|
|
2063
|
-
|
|
2383
|
+
gith stat [--overall | --current-year | --current-month | --current-week | --last-year | --last-month | --last-week] [-limit <n>]
|
|
2384
|
+
fun facts about this branch: biggest commit, busiest day, streaks
|
|
2064
2385
|
|
|
2065
2386
|
Remote and health
|
|
2387
|
+
gith push [--all-branches] push to origin, set tracking, report ahead/behind; never force
|
|
2388
|
+
gith pull [--all-branches] fast-forward from origin; refuse diverged histories
|
|
2066
2389
|
gith sync fetch all remotes and tags, prune stale refs, then sync this branch with origin both ways
|
|
2067
2390
|
gith sync --develop | --main the same for develop or main
|
|
2068
2391
|
gith sync develop:develop two-way sync of one branch with origin: fetch it when behind, push it when ahead, stop when diverged
|
|
2069
2392
|
gith open open origin in the browser
|
|
2070
2393
|
gith doctor check the repo is ready for the branch flow (exit 1 on failures)
|
|
2071
2394
|
gith changelog [--dry-run] add an entry for package.json's version to CHANGELOG.md (commits since the last tag)
|
|
2395
|
+
gith setup -n npm [--force] write a shared pre-commit hook (lint, typecheck, test) for an npm project; --force overwrites it
|
|
2072
2396
|
|
|
2073
2397
|
Help
|
|
2074
2398
|
gith help show this help
|
|
@@ -2077,10 +2401,13 @@ Help
|
|
|
2077
2401
|
--dry-run (any command) print the git commands that would run, without running them
|
|
2078
2402
|
--yes (any command) answer yes to every question (for scripts)
|
|
2079
2403
|
|
|
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
|
-
--
|
|
2404
|
+
Flags: every single-dash flag requires a value; double-dash flags never take one.
|
|
2405
|
+
-b -r -h -l -m -n -limit -message -name -branch -release -hotfix -commit -file -change -days take values
|
|
2406
|
+
--patch --minor --major --local --remote --obsolete --detached --tags --commits --graph --stashes --pockets --branch --regex
|
|
2407
|
+
--develop --main --current-release --current-hotfix --all --&push --all-branches --no-commit --force
|
|
2408
|
+
--update-version --npm release or create release/hotfix: opt in to npm version updates (both required)
|
|
2409
|
+
--overall --current-year --current-month --current-week --last-year --last-month --last-week switches
|
|
2410
|
+
--branch lists removable branches; --release / --hotfix choose a version or branch by command context
|
|
2084
2411
|
-limit caps listings and stat (or -l); -days sets the age for remove --obsolete`;
|
|
2085
2412
|
var args = process.argv.slice(2).filter((arg) => {
|
|
2086
2413
|
if (arg === "--verbose")
|
|
@@ -2137,7 +2464,13 @@ try {
|
|
|
2137
2464
|
undo(parseFlags(rest));
|
|
2138
2465
|
break;
|
|
2139
2466
|
case "save":
|
|
2140
|
-
|
|
2467
|
+
save(parseFlags(rest));
|
|
2468
|
+
break;
|
|
2469
|
+
case "push":
|
|
2470
|
+
push(parseFlags(rest));
|
|
2471
|
+
break;
|
|
2472
|
+
case "pull":
|
|
2473
|
+
pull(parseFlags(rest));
|
|
2141
2474
|
break;
|
|
2142
2475
|
case "squash":
|
|
2143
2476
|
await squash(parseFlags(rest));
|
|
@@ -2161,6 +2494,9 @@ try {
|
|
|
2161
2494
|
case "changelog":
|
|
2162
2495
|
changelog(parseFlags(rest));
|
|
2163
2496
|
break;
|
|
2497
|
+
case "setup":
|
|
2498
|
+
setup(parseFlags(rest));
|
|
2499
|
+
break;
|
|
2164
2500
|
case "doctor":
|
|
2165
2501
|
if (!doctor(parseFlags(rest)))
|
|
2166
2502
|
exit(1);
|
|
@@ -2173,10 +2509,11 @@ try {
|
|
|
2173
2509
|
break;
|
|
2174
2510
|
case "release": {
|
|
2175
2511
|
const flags = parseFlags(rest);
|
|
2512
|
+
assertAllowedFlags(flags, ["patch", "minor", "major", "update-version", "npm"], "release");
|
|
2176
2513
|
const kind = onlyOne(flags, ["patch", "minor", "major"]);
|
|
2177
2514
|
if (!kind)
|
|
2178
|
-
throw new Error("usage: gith release --patch | --minor | --major");
|
|
2179
|
-
release(kind);
|
|
2515
|
+
throw new Error("usage: gith release --patch | --minor | --major [--update-version --npm]");
|
|
2516
|
+
release(kind, flags);
|
|
2180
2517
|
break;
|
|
2181
2518
|
}
|
|
2182
2519
|
case "--version":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tertium/gith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Git helper: branch flow with goal branches, release and hotfix containers, and everyday git commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
"format:check": "biome format src",
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
21
21
|
"prepare": "git config core.hooksPath .githooks || true",
|
|
22
|
-
"release:patch": "node bin/gith.js release --patch",
|
|
23
|
-
"release:minor": "node bin/gith.js release --minor",
|
|
24
|
-
"release:major": "node bin/gith.js release --major",
|
|
25
|
-
"version": "bun run build && node bin/gith.js changelog"
|
|
22
|
+
"release:patch": "node bin/gith.js release --patch --update-version --npm",
|
|
23
|
+
"release:minor": "node bin/gith.js release --minor --update-version --npm",
|
|
24
|
+
"release:major": "node bin/gith.js release --major --update-version --npm",
|
|
25
|
+
"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",
|