@tertium/gith 0.3.3 → 0.4.0

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