@tertium/gith 0.3.0 → 0.3.2
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/LICENSE +18 -12
- package/README.md +20 -8
- package/bin/gith.js +113 -32
- package/package.json +5 -2
package/LICENSE
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 Vitalii Balabanov
|
|
4
4
|
|
|
5
|
-
Permission
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# gith
|
|
1
|
+
# @tertium/gith
|
|
2
2
|
|
|
3
3
|
```
|
|
4
4
|
o
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
d b
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Git helper: the branch flow from [git-convention](https://github.com/Tertiumnon/git-convention) (
|
|
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
15
|
|
|
16
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
17
|
|
|
@@ -26,17 +26,19 @@ gith --version
|
|
|
26
26
|
|
|
27
27
|
From a clone of this repository, build the CLI and link it: `bun install`, `bun run build`, `npm link`.
|
|
28
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
|
+
|
|
29
31
|
## Usage
|
|
30
32
|
|
|
31
33
|
```bash
|
|
32
34
|
# Start and finish work
|
|
33
|
-
gith create -b login-redirect # work branch from develop
|
|
35
|
+
gith create -b login-redirect # work branch develop--login-redirect from develop; stays on the current branch
|
|
34
36
|
gith create -r 1.2.0 # release/1.2.0 from develop
|
|
35
37
|
gith create -h 1.2.1 # hotfix/1.2.1 from main
|
|
36
38
|
gith create --release # release, version = minor after the latest tag
|
|
37
39
|
gith create --hotfix # hotfix, version = patch after the latest tag
|
|
38
40
|
gith switch # list local and origin branches, and switch to the one you pick
|
|
39
|
-
gith switch -b login-redirect # switch to
|
|
41
|
+
gith switch -b login-redirect # switch to develop--login-redirect, creating it from develop if it is missing
|
|
40
42
|
gith switch -commit 3f2a1b9 # detach HEAD at that commit, to look at old code
|
|
41
43
|
gith switch --release # switch to the newest release/* (local or origin); -r 1.9.0 for one version
|
|
42
44
|
gith switch --hotfix # switch to the newest hotfix/* (local or origin); -h 1.2.1 for one version
|
|
@@ -45,7 +47,8 @@ gith merge # this branch into develop
|
|
|
45
47
|
gith finish --current-release # release the newest release/*: version, changelog, merge to main, tag, merge main to develop, then ask before deleting the branch
|
|
46
48
|
gith finish --current-hotfix # the same for the newest hotfix/*
|
|
47
49
|
gith merge # pick the branch to merge, then the target from the list
|
|
48
|
-
gith merge
|
|
50
|
+
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)
|
|
49
52
|
gith merge -h 1.2.1 # this branch into hotfix/1.2.1
|
|
50
53
|
gith merge --current-release # this branch into the newest release/* (local or origin)
|
|
51
54
|
gith merge --current-hotfix # this branch into the newest hotfix/* (local or origin)
|
|
@@ -55,7 +58,7 @@ gith release --major # npm version major, from develop into main
|
|
|
55
58
|
|
|
56
59
|
# Fix history
|
|
57
60
|
gith undo # undo the last commit, keep its changes staged (not if pushed)
|
|
58
|
-
gith save [-m "msg"]
|
|
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
|
|
59
62
|
gith squash -limit 3 [-m "msg"] # combine the last 3 commits into one (keeps the oldest message)
|
|
60
63
|
|
|
61
64
|
# Save and reset work
|
|
@@ -98,6 +101,7 @@ gith stat --last-week -limit 50 # the same for last week, looking at no more
|
|
|
98
101
|
|
|
99
102
|
# Remote and health
|
|
100
103
|
gith sync # fetch all remotes and tags, prune stale refs
|
|
104
|
+
gith sync develop:develop # two-way sync of one branch with origin
|
|
101
105
|
gith open # open origin in the browser
|
|
102
106
|
gith doctor # check the repo is ready for the branch flow
|
|
103
107
|
gith changelog --dry-run # preview the CHANGELOG.md entry for package.json's version
|
|
@@ -120,9 +124,12 @@ One rule applies to every command: **a single dash always takes a value, and a d
|
|
|
120
124
|
| `-b <name>`, `-branch <name>` | value | `create`, `switch`, `remove` |
|
|
121
125
|
| `--branch` | alone: open the picker | `remove` |
|
|
122
126
|
| `-r <version>`, `-release <version>` | value | `create`, `merge` |
|
|
127
|
+
| `-r` alone | the only release, or a pick from the list | `merge` |
|
|
123
128
|
| `--release` | alone: version from the latest tag | `create` |
|
|
129
|
+
| `--main` | alone: merge into main | `merge` |
|
|
124
130
|
| `--current-release`, `--current-hotfix` | switch: merge into the newest release or hotfix | `merge` |
|
|
125
131
|
| `-h <version>`, `-hotfix <version>` | value | `create`, `merge` |
|
|
132
|
+
| `-h` alone | the only hotfix, or a pick from the list | `merge` |
|
|
126
133
|
| `--hotfix` | alone: version from the latest tag | `create` |
|
|
127
134
|
| `-limit <n>` | value | `squash`: how many commits to combine |
|
|
128
135
|
| `-m <msg>`, `-message <msg>` | value | `squash` |
|
|
@@ -145,7 +152,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
145
152
|
|
|
146
153
|
## Git commands
|
|
147
154
|
|
|
148
|
-
- **`switch -b`** switches to a branch. If
|
|
155
|
+
- **`switch -b`** switches to a branch. If a branch with that name exists (locally or on origin), it is checked out. Otherwise, if `develop--<name>` exists, that one is checked out. If neither exists, `develop--<name>` is created from `origin/develop`, the same way as `create -b`, and checked out.
|
|
149
156
|
- **`switch -commit <sha>`** detaches HEAD at that commit so you can look at old code. It refuses a commit that does not exist, and it refuses while tracked files have changes (untracked files do not block it). It lists the files that block it, and points to `gith stash`. On a detached HEAD, `gith switch -b <name>` creates the branch at the current commit, so commits made there stay on it. It does not create or switch branches for other commits, so commits made on a detached HEAD are easy to lose: name a branch before you leave.
|
|
150
157
|
- **`remove -b <branch>`** removes the branch on both sides: the local branch first (with `git branch -d`, so git refuses to delete an unmerged branch), then the origin branch. A side that does not exist is skipped, so a branch that lives only here is still removed. A name that exists in neither place is an error. `--local` touches only this clone; `--remote` touches only origin. Containers (`develop`, `main`, `release/*`, `hotfix/*`) are refused.
|
|
151
158
|
- **`remove`** with no name opens the list of branches and asks which to remove, the same as `remove --branch`. By default it lists branch pairs, meaning branches that exist both here and on origin. `--local` lists local branches and `--remote` lists origin branches. Each choice is removed on the same sides as above. `-limit <n>` shows only the first `n` (all by default), and the picker numbers only those. Pick with the arrow keys: up and down move, space checks an item, enter confirms the checked items, and esc cancels. `switch` takes one item with enter. The list needs a terminal, and that is checked before anything is listed. The checked-out branch and containers are never listed. Choosing items is the confirmation, so nothing else is asked. If a delete fails (for example, an unmerged branch), the removal stops there and earlier picks stay deleted.
|
|
@@ -171,6 +178,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
171
178
|
- **`open`** opens origin in the browser, turning an `ssh` or `https` remote into a web address. GitHub and GitLab style addresses work.
|
|
172
179
|
- **`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.
|
|
173
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.
|
|
174
182
|
- **`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.
|
|
175
183
|
- **`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.
|
|
176
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.
|
|
@@ -179,7 +187,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
179
187
|
|
|
180
188
|
## What it does
|
|
181
189
|
|
|
182
|
-
- **`create -b`** creates
|
|
190
|
+
- **`create -b`** creates `develop--<name>` from `origin/develop`, pushes it with upstream tracking, and leaves the current branch checked out. The prefix shows the source branch; `create -b develop` and `create -b main` are refused. It does not touch the working tree, so it works with uncommitted changes.
|
|
183
191
|
- **`create -r` / `create -h`** creates a container from its base (`develop` for release, `main` for hotfix), refusing names that already exist on origin. Like `create -b`, it does not switch.
|
|
184
192
|
- **With `--release` or `--hotfix` (no value)**, the version is worked out from the highest `vX.Y.Z` tag across local tags and origin: `-r` takes the next minor (`1.2.1` → `1.3.0`), `-h` takes the next patch (`1.2.1` → `1.2.2`). With no tags yet, the first release is `0.1.0` and the first hotfix `0.0.1`. The version is printed before the branch is created.
|
|
185
193
|
- **`merge --current-release`** and **`--current-hotfix`** pick the newest `release/*` or `hotfix/*` branch by version (numbers compare as numbers, so `1.10.0` beats `1.9.0`), checking both local branches and origin, and merge into it. The target must exist on origin, because the merge pushes it; a newest branch that only exists here stops the command with a message.
|
|
@@ -215,3 +223,7 @@ bun run typecheck # tsc --noEmit
|
|
|
215
223
|
## Publishing
|
|
216
224
|
|
|
217
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
|
+
## Support
|
|
228
|
+
|
|
229
|
+
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", "
|
|
10
|
-
["hotfix", "
|
|
9
|
+
["release", "optional"],
|
|
10
|
+
["hotfix", "optional"],
|
|
11
11
|
["limit", "value"],
|
|
12
12
|
["message", "value"],
|
|
13
13
|
["name", "value"],
|
|
@@ -29,6 +29,7 @@ var SPECS = new Map([
|
|
|
29
29
|
["current-release", "switch"],
|
|
30
30
|
["current-hotfix", "switch"],
|
|
31
31
|
["develop", "switch"],
|
|
32
|
+
["main", "switch"],
|
|
32
33
|
["pockets", "switch"],
|
|
33
34
|
["detached", "switch"],
|
|
34
35
|
["all", "switch"],
|
|
@@ -89,10 +90,14 @@ function parseFlags(argv) {
|
|
|
89
90
|
if (style === "single") {
|
|
90
91
|
if (kind === "switch")
|
|
91
92
|
throw new Error(`${token} is a switch; write --${name}.`);
|
|
92
|
-
if (
|
|
93
|
+
if (hasNext) {
|
|
94
|
+
flags.set(name, next);
|
|
95
|
+
i++;
|
|
96
|
+
} else if (kind === "optional") {
|
|
97
|
+
flags.set(name, "");
|
|
98
|
+
} else {
|
|
93
99
|
throw new Error(`${token} needs a value.`);
|
|
94
|
-
|
|
95
|
-
i++;
|
|
100
|
+
}
|
|
96
101
|
} else if (kind === "switch") {
|
|
97
102
|
flags.set(name, "true");
|
|
98
103
|
} else if (kind === "value") {
|
|
@@ -163,6 +168,14 @@ function capture(args) {
|
|
|
163
168
|
function hasOnOrigin(branch) {
|
|
164
169
|
return capture(`ls-remote --heads origin ${branch}`) !== "";
|
|
165
170
|
}
|
|
171
|
+
function isAncestor(ancestor, descendant) {
|
|
172
|
+
try {
|
|
173
|
+
captureArgs(["merge-base", "--is-ancestor", ancestor, descendant]);
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
166
179
|
function listTags() {
|
|
167
180
|
const local = capture("tag --list").split(`
|
|
168
181
|
`);
|
|
@@ -548,7 +561,7 @@ async function select(question, options, multi) {
|
|
|
548
561
|
|
|
549
562
|
// src/components/rules/rules.constants.ts
|
|
550
563
|
var CONTAINER = /^(develop|main|release\/.+|hotfix\/.+)$/;
|
|
551
|
-
var TARGET = /^(develop|release\/.+|hotfix\/.+)$/;
|
|
564
|
+
var TARGET = /^(main|develop|release\/.+|hotfix\/.+)$/;
|
|
552
565
|
|
|
553
566
|
// src/components/rules/rules.ts
|
|
554
567
|
function isContainer(name) {
|
|
@@ -597,7 +610,7 @@ function nextVersion(tags, bump) {
|
|
|
597
610
|
}
|
|
598
611
|
|
|
599
612
|
// src/components/flow/flow.utils.ts
|
|
600
|
-
function
|
|
613
|
+
function containers(kind) {
|
|
601
614
|
const prefix = `${kind}/`;
|
|
602
615
|
const local = captureArgs([
|
|
603
616
|
"for-each-ref",
|
|
@@ -608,15 +621,19 @@ function newestContainer(kind) {
|
|
|
608
621
|
const origin = captureArgs(["ls-remote", "--heads", "origin"]).split(`
|
|
609
622
|
`).map((line) => (line.split("\t")[1] ?? "").replace(/^refs\/heads\//, ""));
|
|
610
623
|
const candidates = new Set([...local, ...origin].filter((name) => name.startsWith(prefix)));
|
|
611
|
-
|
|
624
|
+
const versioned = [];
|
|
612
625
|
for (const name of candidates) {
|
|
613
626
|
const version = parseSemver(name.slice(prefix.length));
|
|
614
|
-
if (version
|
|
615
|
-
|
|
627
|
+
if (version)
|
|
628
|
+
versioned.push({ name, version });
|
|
616
629
|
}
|
|
630
|
+
return versioned.sort((a, b) => compare(b.version, a.version)).map((item) => item.name);
|
|
631
|
+
}
|
|
632
|
+
function newestContainer(kind) {
|
|
633
|
+
const [newest] = containers(kind);
|
|
617
634
|
if (!newest)
|
|
618
635
|
throw new Error(`no ${kind} branch exists, locally or on origin.`);
|
|
619
|
-
return newest
|
|
636
|
+
return newest;
|
|
620
637
|
}
|
|
621
638
|
function nextContainerVersion(kind) {
|
|
622
639
|
const bump = kind === "release" ? "minor" : "patch";
|
|
@@ -658,6 +675,7 @@ async function merge(flags) {
|
|
|
658
675
|
return mergePick();
|
|
659
676
|
const kind = onlyOne(flags, [
|
|
660
677
|
"develop",
|
|
678
|
+
"main",
|
|
661
679
|
"release",
|
|
662
680
|
"hotfix",
|
|
663
681
|
"current-release",
|
|
@@ -666,31 +684,50 @@ async function merge(flags) {
|
|
|
666
684
|
let target = "develop";
|
|
667
685
|
if (kind === "develop")
|
|
668
686
|
target = "develop";
|
|
687
|
+
else if (kind === "main")
|
|
688
|
+
target = "main";
|
|
669
689
|
else if (kind === "current-release")
|
|
670
690
|
target = newestContainer("release");
|
|
671
691
|
else if (kind === "current-hotfix")
|
|
672
692
|
target = newestContainer("hotfix");
|
|
673
693
|
else if (kind) {
|
|
674
694
|
const version = flags.get(kind) ?? "";
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
target =
|
|
695
|
+
const picked = version === "" ? await pickContainer(kind) : containerName(kind, version);
|
|
696
|
+
if (picked === undefined)
|
|
697
|
+
return;
|
|
698
|
+
target = picked;
|
|
679
699
|
}
|
|
680
700
|
await mergeInto(target);
|
|
681
701
|
}
|
|
702
|
+
async function pickContainer(kind) {
|
|
703
|
+
const names = containers(kind);
|
|
704
|
+
if (names.length === 0)
|
|
705
|
+
throw new Error(`no ${kind} branch exists, locally or on origin.`);
|
|
706
|
+
if (names.length === 1)
|
|
707
|
+
return names[0];
|
|
708
|
+
if (!process.stdin.isTTY) {
|
|
709
|
+
throw new Error(`several ${kind} branches exist; pass -${kind === "release" ? "r" : "h"} <version> instead; the list needs a terminal`);
|
|
710
|
+
}
|
|
711
|
+
const [picked] = await select(`Merge into which ${kind}?`, names.map((name) => ({ name, label: name })), false);
|
|
712
|
+
return picked;
|
|
713
|
+
}
|
|
714
|
+
function workBranchName(goal, from) {
|
|
715
|
+
return from === undefined ? `develop--${goal}` : goal;
|
|
716
|
+
}
|
|
682
717
|
function createBranch(goal, from) {
|
|
683
718
|
if (isContainer(goal))
|
|
684
719
|
throw new Error(`'${goal}' is a container name; pick a goal.`);
|
|
685
|
-
|
|
686
|
-
|
|
720
|
+
const name = workBranchName(goal, from);
|
|
721
|
+
if (hasOnOrigin(name))
|
|
722
|
+
throw new Error(`'${name}' already exists on origin.`);
|
|
687
723
|
if (from === undefined) {
|
|
688
724
|
gitArgs(["fetch", "origin", "develop"]);
|
|
689
|
-
gitArgs(["branch",
|
|
725
|
+
gitArgs(["branch", name, "origin/develop"]);
|
|
690
726
|
} else {
|
|
691
|
-
gitArgs(["branch",
|
|
727
|
+
gitArgs(["branch", name, from]);
|
|
692
728
|
}
|
|
693
|
-
gitArgs(["push", "-u", "origin",
|
|
729
|
+
gitArgs(["push", "-u", "origin", name]);
|
|
730
|
+
return name;
|
|
694
731
|
}
|
|
695
732
|
async function mergeInto(target, from) {
|
|
696
733
|
if (!isTarget(target))
|
|
@@ -889,10 +926,12 @@ function undo(flags) {
|
|
|
889
926
|
}
|
|
890
927
|
async function save(flags) {
|
|
891
928
|
const message = flags.get("message");
|
|
892
|
-
const extra = [...flags.keys()].filter((key) => key !== "message");
|
|
929
|
+
const extra = [...flags.keys()].filter((key) => key !== "message" && key !== "all");
|
|
893
930
|
if (extra.length > 0)
|
|
894
|
-
throw new Error("usage: gith save [-m <message>]");
|
|
931
|
+
throw new Error("usage: gith save [-m <message>] [--all]");
|
|
895
932
|
const branch = currentBranch();
|
|
933
|
+
if (flags.has("all"))
|
|
934
|
+
gitArgs(["add", "-A"]);
|
|
896
935
|
if (captureArgs(["diff", "--cached", "--name-only"]) === "") {
|
|
897
936
|
throw new Error("nothing staged; stage the changes to add to the last commit.");
|
|
898
937
|
}
|
|
@@ -1163,10 +1202,14 @@ async function switchBranch(flags) {
|
|
|
1163
1202
|
return;
|
|
1164
1203
|
}
|
|
1165
1204
|
const branch = requireBranch(flags, "switch");
|
|
1166
|
-
|
|
1167
|
-
|
|
1205
|
+
const from = isDetached() ? "HEAD" : undefined;
|
|
1206
|
+
const exists = (name) => hasLocal(name) || hasOnOrigin(name);
|
|
1207
|
+
let name = branch;
|
|
1208
|
+
if (!exists(branch)) {
|
|
1209
|
+
const made = workBranchName(branch, from);
|
|
1210
|
+
name = exists(made) ? made : createBranch(branch, from);
|
|
1168
1211
|
}
|
|
1169
|
-
gitArgs(["switch", "--",
|
|
1212
|
+
gitArgs(["switch", "--", name]);
|
|
1170
1213
|
}
|
|
1171
1214
|
function candidates(scope, local, remote, current) {
|
|
1172
1215
|
const notContainer = (name) => !isContainer(name);
|
|
@@ -1259,6 +1302,39 @@ function sync(flags) {
|
|
|
1259
1302
|
noFlags2(flags, "sync");
|
|
1260
1303
|
gitArgs(["fetch", "--all", "--tags", "--prune"]);
|
|
1261
1304
|
}
|
|
1305
|
+
function syncBranch(args) {
|
|
1306
|
+
const [spec = ""] = args;
|
|
1307
|
+
const [name = "", other = name] = spec.split(":");
|
|
1308
|
+
if (args.length !== 1 || other !== name || name === "")
|
|
1309
|
+
throw new Error("usage: gith sync <branch>:<branch>, the same name on both sides.");
|
|
1310
|
+
assertBranchName(name);
|
|
1311
|
+
const local = hasLocal(name);
|
|
1312
|
+
const remote = hasOnOrigin(name);
|
|
1313
|
+
if (!local && !remote)
|
|
1314
|
+
throw new Error(`'${name}' does not exist, locally or on origin.`);
|
|
1315
|
+
if (!remote) {
|
|
1316
|
+
gitArgs(["push", "origin", `${name}:${name}`]);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
gitArgs(["fetch", "origin", name]);
|
|
1320
|
+
if (!local) {
|
|
1321
|
+
gitArgs(["fetch", "origin", `${name}:${name}`]);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const here = capture(`rev-parse refs/heads/${name}`);
|
|
1325
|
+
const there = capture(`rev-parse refs/remotes/origin/${name}`);
|
|
1326
|
+
if (here === there) {
|
|
1327
|
+
console.log(`'${name}' is already in sync with origin.`);
|
|
1328
|
+
} else if (isAncestor(there, here)) {
|
|
1329
|
+
gitArgs(["push", "origin", `${name}:${name}`]);
|
|
1330
|
+
} else if (!isAncestor(here, there)) {
|
|
1331
|
+
throw new Error(`'${name}' has diverged from origin/${name}; merge or rebase one side first.`);
|
|
1332
|
+
} else if (currentBranch2() === name) {
|
|
1333
|
+
gitArgs(["merge", "--ff-only", `origin/${name}`]);
|
|
1334
|
+
} else {
|
|
1335
|
+
gitArgs(["fetch", "origin", `${name}:${name}`]);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1262
1338
|
async function reset(flags) {
|
|
1263
1339
|
noFlags2(flags, "reset");
|
|
1264
1340
|
const changes = captureArgs(["status", "--short"]).split(`
|
|
@@ -1928,26 +2004,27 @@ function versionText() {
|
|
|
1928
2004
|
var USAGE = `gith - git helper: branch flow and everyday git commands
|
|
1929
2005
|
|
|
1930
2006
|
Start and finish work
|
|
1931
|
-
gith create -b <branch> work branch from develop
|
|
2007
|
+
gith create -b <branch> work branch develop--<branch> from develop (no switch)
|
|
1932
2008
|
gith create -r <version> release/<version> from develop
|
|
1933
2009
|
gith create -h <version> hotfix/<version> from main
|
|
1934
2010
|
gith create --release | --hotfix version from the latest tag: minor for release, patch for hotfix
|
|
1935
2011
|
gith switch list local and origin branches, and switch to the one picked
|
|
1936
|
-
gith switch -b <branch> switch to a branch, creating
|
|
2012
|
+
gith switch -b <branch> switch to a branch, creating develop--<branch> from develop if missing (on a detached HEAD: <branch> from that commit)
|
|
1937
2013
|
gith switch -commit <sha> detach HEAD at a commit, to look at it (tracked files must be clean)
|
|
1938
|
-
gith switch --release | --hotfix switch to the newest release/* or hotfix/* (local or origin); -r <v> / -h <v> for one version
|
|
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
|
|
1939
2015
|
gith update merge origin/develop into this work branch
|
|
1940
2016
|
gith finish --current-release | --current-hotfix release the newest release/* or hotfix/*: version, changelog, merge to main, tag, back to develop
|
|
1941
2017
|
gith merge --develop merge this branch into develop
|
|
1942
|
-
gith merge
|
|
1943
|
-
gith merge
|
|
2018
|
+
gith merge --main merge this branch into main
|
|
2019
|
+
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
|
|
1944
2021
|
gith merge --current-release merge this branch into the newest release (local or origin)
|
|
1945
2022
|
gith merge --current-hotfix merge this branch into the newest hotfix (local or origin)
|
|
1946
2023
|
gith release --patch | --minor | --major bump the version and publish
|
|
1947
2024
|
|
|
1948
2025
|
Fix history
|
|
1949
2026
|
gith undo undo the last commit, keep its changes staged (not if pushed)
|
|
1950
|
-
gith save [-m <msg>]
|
|
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)
|
|
1951
2028
|
gith squash -limit <n> [-m <msg>] combine the last n commits into one (keeps the oldest message)
|
|
1952
2029
|
|
|
1953
2030
|
Save and reset work
|
|
@@ -1983,6 +2060,7 @@ Look at the repo
|
|
|
1983
2060
|
|
|
1984
2061
|
Remote and health
|
|
1985
2062
|
gith sync fetch all remotes and tags, prune stale refs
|
|
2063
|
+
gith sync develop:develop two-way sync of one branch with origin: fetch it when behind, push it when ahead, stop when diverged
|
|
1986
2064
|
gith open open origin in the browser
|
|
1987
2065
|
gith doctor check the repo is ready for the branch flow (exit 1 on failures)
|
|
1988
2066
|
gith changelog [--dry-run] add an entry for package.json's version to CHANGELOG.md (commits since the last tag)
|
|
@@ -2032,7 +2110,10 @@ try {
|
|
|
2032
2110
|
await remove(parseFlags(rest));
|
|
2033
2111
|
break;
|
|
2034
2112
|
case "sync":
|
|
2035
|
-
|
|
2113
|
+
if (rest[0] !== undefined && !rest[0].startsWith("-"))
|
|
2114
|
+
syncBranch(rest);
|
|
2115
|
+
else
|
|
2116
|
+
sync(parseFlags(rest));
|
|
2036
2117
|
break;
|
|
2037
2118
|
case "clean":
|
|
2038
2119
|
await clean(parseFlags(rest));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tertium/gith",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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": {
|
|
@@ -15,13 +15,16 @@
|
|
|
15
15
|
"test": "bun test --no-coverage",
|
|
16
16
|
"lint": "biome check src",
|
|
17
17
|
"lint:fix": "biome check --write src",
|
|
18
|
+
"format": "biome format --write src",
|
|
19
|
+
"format:check": "biome format src",
|
|
18
20
|
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepare": "git config core.hooksPath .githooks || true",
|
|
19
22
|
"release:patch": "node bin/gith.js release --patch",
|
|
20
23
|
"release:minor": "node bin/gith.js release --minor",
|
|
21
24
|
"release:major": "node bin/gith.js release --major",
|
|
22
25
|
"version": "bun run build && node bin/gith.js changelog"
|
|
23
26
|
},
|
|
24
|
-
"license": "
|
|
27
|
+
"license": "MIT",
|
|
25
28
|
"author": "Vitalii Balabanov",
|
|
26
29
|
"repository": {
|
|
27
30
|
"type": "git",
|