@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.
- package/README.md +8 -32
- package/bin/gith.js +38 -33
- 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
|
|
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`**
|
|
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`)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
963
|
-
|
|
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 (
|
|
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
|
-
|
|
1303
|
-
|
|
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]
|
|
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)
|