@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.
Files changed (3) hide show
  1. package/README.md +41 -49
  2. package/bin/gith.js +361 -110
  3. 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
- From a clone of this repository, build the CLI and link it: `bun install`, `bun run build`, `npm link`.
18
+ See [Command design rules](docs/RULES.md) before changing or adding commands and flags.
28
19
 
29
- `bun install` also points git at `.githooks/`, so each commit runs `bun run typecheck`, `bun run format:check` and `bun run lint` first, and a failing check stops it. `bun run format` rewrites files to the formatter's style.
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, version = minor after the latest tag
39
- gith create --hotfix # hotfix, version = patch after the latest tag
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 # this branch into develop
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 (-r alone: the only release, or pick from the list)
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 [-m "msg"] [--all] # --all stages every change first; fold staged changes into the last commit, or make a new commit with them if the branch has none yet (asks for a message); pushes if origin differs
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 sync # fetch all remotes and tags, prune stale refs
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, and `--limit 10` and `-remote` are errors that name the right spelling.
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
- | `-r` alone | the only release, or a pick from the list | `merge` |
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
- | `-h` alone | the only hotfix, or a pick from the list | `merge` |
133
- | `--hotfix` | alone: version from the latest tag | `create` |
134
- | `-limit <n>` | value | `squash`: how many commits to combine |
135
- | `-m <msg>`, `-message <msg>` | value | `squash` |
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` only applies to `squash`.
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
- - **`save`** saves the staged changes. If the branch has commits of its own (commits origin's `develop`, or `main`, lacks), it folds them into the last commit with `git commit --amend --no-edit`, keeping its message. `-m` replaces the message. If the branch has no commits of its own, it never touches the base commit: it creates a new commit, asking for its message (an empty answer is refused, and `-m` skips the question). It refuses with nothing staged. It pushes only when origin already has this branch and its copy differs from the new commit: a plain `git push origin <branch>` when the commit only moves forward, or, when a pushed commit was rewritten, a `Save it and force-push ...? [Y/n]` question (Enter means yes; a no changes nothing) followed by `git push --force-with-lease origin <branch>`, which refuses if origin moved since your last fetch. If origin has no such branch, the save stays local. The questions need a terminal, so a script cannot force-push or commit by accident.
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`) syncs one branch with origin in both directions. 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.
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 the branch graph instead. 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.
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", "optional"],
10
- ["hotfix", "optional"],
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-]*)$/.exec(token);
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 capture(`ls-remote --heads origin ${branch}`) !== "";
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 [version] | -h [version]");
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
- function remoteSha(branch) {
894
- return captureArgs(["ls-remote", "--heads", "origin", branch]).split("\t")[0] ?? "";
895
- }
896
- async function askMessage() {
897
- const text = await ask("Commit message: ");
898
- if (text === "")
899
- throw new Error("a commit needs a message; nothing was committed.");
900
- return text;
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
- async function save(flags) {
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("usage: gith save [-m <message>] [--all]");
932
- const branch = currentBranch();
933
- if (flags.has("all"))
934
- gitArgs(["add", "-A"]);
935
- if (captureArgs(["diff", "--cached", "--name-only"]) === "") {
936
- throw new Error("nothing staged; stage the changes to add to the last commit.");
937
- }
938
- const owned = hasOwnCommit();
939
- const pushed = captureArgs(["branch", "-r", "--contains", "HEAD"]) !== "";
940
- const rewrite = owned && pushed;
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 (!isDryRun() && captureArgs(["rev-parse", "HEAD"]) === remote) {
963
- console.log("origin already has this commit; not pushed.");
964
- return;
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
- if (rewrite)
967
- gitArgs(["push", "--force-with-lease", "origin", branch]);
968
- else
969
- gitArgs(["push", "origin", branch]);
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
- function commits(limit, graph) {
1117
+ var commits = (limit, graph) => {
1036
1118
  if (!hasHead()) {
1037
- console.log("No commits.");
1119
+ process.stdout.write(`No commits.
1120
+ `);
1038
1121
  return;
1039
1122
  }
1040
- const shape = graph ? ["--graph", "--oneline", "--decorate"] : ["--date=short", "--format=%h %ad %an %s"];
1041
- console.log(captureArgs(["log", `-n${limit}`, ...shape]));
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
- noFlags2(flags, "sync");
1303
- gitArgs(["fetch", "--all", "--tags", "--prune"]);
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 readFileSync4 } from "node:fs";
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 existsSync3, readFileSync as readFileSync3 } from "node:fs";
1973
- import path2 from "node:path";
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 = path2.dirname(fileURLToPath(import.meta.url));
2210
+ let dir = path4.dirname(fileURLToPath(import.meta.url));
1977
2211
  for (;; ) {
1978
- const candidate = path2.join(dir, "package.json");
1979
- if (existsSync3(candidate)) {
1980
- const pkg = JSON.parse(readFileSync3(candidate, "utf8"));
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 = path2.dirname(dir);
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(readFileSync4(packageJsonPath(), "utf8"));
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: minor for release, patch for hotfix
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 / -h do the same; -r <v> / -h <v> for one version
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 [-r <v> | -h <v>] merge this branch into release/<v> or hotfix/<v>; without a version, the only one or a pick from the list
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 [-m <msg>] [--all] fold staged changes (--all stages every change first) into the last commit, or commit them if the branch has none yet (asks for a message); pushes if origin differs (asks before a force-push)
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 [period] [-limit <n>] fun facts about this branch: biggest commit, busiest day, streaks
2059
- period: --overall (default), --current-year|-month|-week, --last-year|-month|-week
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 sync fetch all remotes and tags, prune stale refs
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: a single dash always takes a value, a double dash never does.
2076
- -b -r -h -l -m -n -limit -message -name -release -hotfix -branch take a value, e.g. -limit 10
2077
- --patch --minor --major --local --remote --obsolete --detached --tags --commits --graph --stashes --pockets switches
2078
- --branch --release --hotfix alone pick automatically (picker, latest tag)
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
- await save(parseFlags(rest));
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.2",
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",