@tertium/gith 0.3.2 → 0.3.3

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 +8 -32
  2. package/bin/gith.js +38 -33
  3. package/package.json +1 -1
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
  /
@@ -15,19 +17,6 @@ Git helper: the branch flow from [git-convention](https://github.com/Tertiumnon/
15
17
 
16
18
  > **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
19
 
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
- ```
26
-
27
- From a clone of this repository, build the CLI and link it: `bun install`, `bun run build`, `npm link`.
28
-
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.
30
-
31
20
  ## Usage
32
21
 
33
22
  ```bash
@@ -58,7 +47,7 @@ gith release --major # npm version major, from develop into main
58
47
 
59
48
  # Fix history
60
49
  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
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)
62
51
  gith squash -limit 3 [-m "msg"] # combine the last 3 commits into one (keeps the oldest message)
63
52
 
64
53
  # Save and reset work
@@ -100,7 +89,8 @@ gith stat # fun facts about this branch: biggest commi
100
89
  gith stat --last-week -limit 50 # the same for last week, looking at no more than 50 commits
101
90
 
102
91
  # Remote and health
103
- gith sync # fetch all remotes and tags, prune stale refs
92
+ gith sync # fetch all remotes and tags, prune stale refs, then sync this branch with origin both ways
93
+ gith sync --develop | --main # the same for develop or main
104
94
  gith sync develop:develop # two-way sync of one branch with origin
105
95
  gith open # open origin in the browser
106
96
  gith doctor # check the repo is ready for the branch flow
@@ -159,7 +149,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
159
149
  - **`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
150
  - **`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
151
  - **`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.
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.
163
153
  - **`stash`** saves changes with `git stash push --include-untracked`. `-n` names the stash. It refuses when there is nothing to save.
164
154
  - **`find`** (also `search`) lists the newest commits on this branch that match every filter given:
165
155
  - `-commit <text>` matches the commit message, ignoring case, and the text is literal.
@@ -177,8 +167,8 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
177
167
  - **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
168
  - **`open`** opens origin in the browser, turning an `ssh` or `https` remote into a web address. GitHub and GitLab style addresses work.
179
169
  - **`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.
170
+ - **`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
+ - **`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
172
  - **`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
173
  - **`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
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.
@@ -210,20 +200,6 @@ Both need a clean working tree and a `package.json` with a version, because they
210
200
 
211
201
  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
202
 
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
203
  ## Support
228
204
 
229
205
  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
@@ -474,8 +474,10 @@ async function confirm(question) {
474
474
  console.log(`dry run: would ask: ${question}`);
475
475
  return true;
476
476
  }
477
- if (assumeYes)
477
+ if (assumeYes) {
478
+ console.log(`${question}yes (--yes)`);
478
479
  return true;
480
+ }
479
481
  if (!process.stdin.isTTY)
480
482
  throw new Error("this needs a terminal to confirm; pass --yes to skip the question.");
481
483
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -930,43 +932,36 @@ async function save(flags) {
930
932
  if (extra.length > 0)
931
933
  throw new Error("usage: gith save [-m <message>] [--all]");
932
934
  const branch = currentBranch();
933
- if (flags.has("all"))
935
+ const unstaged = captureArgs(["add", "-A", "--dry-run"]).split(`
936
+ `).filter(Boolean);
937
+ const stageAll = flags.has("all") || unstaged.length === 1 || unstaged.length > 1 && await confirm(`Stage all ${unstaged.length} changes first? [Y/n] `);
938
+ if (stageAll && unstaged.length > 0) {
939
+ console.log(`Staging ${unstaged.length} unstaged change(s):`);
940
+ for (const line of unstaged)
941
+ console.log(` ${line}`);
942
+ }
943
+ if (stageAll)
934
944
  gitArgs(["add", "-A"]);
935
945
  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
- }
946
+ throw new Error("nothing staged; stage the changes to save.");
951
947
  }
952
- if (owned) {
948
+ const previous = captureArgs(["rev-parse", "HEAD"]);
949
+ const amend = hasOwnCommit() && await confirm("Amend the previous commit? [Y/n] ");
950
+ if (amend) {
953
951
  gitArgs(message ? ["commit", "--amend", "-m", message] : ["commit", "--amend", "--no-edit"]);
954
952
  } 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;
953
+ gitArgs(["commit", "-m", message ?? await askMessage()]);
961
954
  }
962
- if (!isDryRun() && captureArgs(["rev-parse", "HEAD"]) === remote) {
963
- console.log("origin already has this commit; not pushed.");
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.");
964
959
  return;
965
960
  }
966
- if (rewrite)
961
+ if (rewrites)
967
962
  gitArgs(["push", "--force-with-lease", "origin", branch]);
968
963
  else
969
- gitArgs(["push", "origin", branch]);
964
+ gitArgs(["push", "-u", "origin", branch]);
970
965
  }
971
966
  async function squash(flags) {
972
967
  const count = parseLimit(flags);
@@ -1299,15 +1294,25 @@ async function remove(flags) {
1299
1294
  await removeBranches(scope, await pick(scope, parseLimit(flags)));
1300
1295
  }
1301
1296
  function sync(flags) {
1302
- noFlags2(flags, "sync");
1303
- gitArgs(["fetch", "--all", "--tags", "--prune"]);
1297
+ const kind = onlyOne(flags, ["develop", "main"]);
1298
+ const extra = [...flags.keys()].filter((key) => key !== kind);
1299
+ if (extra.length > 0)
1300
+ throw new Error("usage: gith sync [--develop | --main]");
1301
+ const name = kind ?? currentBranch2();
1302
+ if (name === "HEAD")
1303
+ throw new Error("HEAD is detached; check out a branch, or use gith sync --develop | --main.");
1304
+ syncWithOrigin(name);
1304
1305
  }
1305
1306
  function syncBranch(args) {
1306
1307
  const [spec = ""] = args;
1307
1308
  const [name = "", other = name] = spec.split(":");
1308
1309
  if (args.length !== 1 || other !== name || name === "")
1309
1310
  throw new Error("usage: gith sync <branch>:<branch>, the same name on both sides.");
1311
+ syncWithOrigin(name);
1312
+ }
1313
+ function syncWithOrigin(name) {
1310
1314
  assertBranchName(name);
1315
+ gitArgs(["fetch", "--all", "--tags", "--prune"]);
1311
1316
  const local = hasLocal(name);
1312
1317
  const remote = hasOnOrigin(name);
1313
1318
  if (!local && !remote)
@@ -1316,7 +1321,6 @@ function syncBranch(args) {
1316
1321
  gitArgs(["push", "origin", `${name}:${name}`]);
1317
1322
  return;
1318
1323
  }
1319
- gitArgs(["fetch", "origin", name]);
1320
1324
  if (!local) {
1321
1325
  gitArgs(["fetch", "origin", `${name}:${name}`]);
1322
1326
  return;
@@ -2024,7 +2028,7 @@ Start and finish work
2024
2028
 
2025
2029
  Fix history
2026
2030
  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)
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
2028
2032
  gith squash -limit <n> [-m <msg>] combine the last n commits into one (keeps the oldest message)
2029
2033
 
2030
2034
  Save and reset work
@@ -2059,7 +2063,8 @@ Look at the repo
2059
2063
  period: --overall (default), --current-year|-month|-week, --last-year|-month|-week
2060
2064
 
2061
2065
  Remote and health
2062
- gith sync fetch all remotes and tags, prune stale refs
2066
+ gith sync fetch all remotes and tags, prune stale refs, then sync this branch with origin both ways
2067
+ gith sync --develop | --main the same for develop or main
2063
2068
  gith sync develop:develop two-way sync of one branch with origin: fetch it when behind, push it when ahead, stop when diverged
2064
2069
  gith open open origin in the browser
2065
2070
  gith doctor check the repo is ready for the branch flow (exit 1 on failures)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tertium/gith",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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": {