@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.
Files changed (4) hide show
  1. package/LICENSE +18 -12
  2. package/README.md +20 -8
  3. package/bin/gith.js +113 -32
  4. package/package.json +5 -2
package/LICENSE CHANGED
@@ -1,15 +1,21 @@
1
- ISC License
1
+ MIT License
2
2
 
3
3
  Copyright (c) 2026 Vitalii Balabanov
4
4
 
5
- Permission to use, copy, modify, and/or distribute this software for any
6
- purpose with or without fee is hereby granted, provided that the above
7
- copyright notice and this permission notice appear in all copies.
8
-
9
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
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) (goal-named work branches, `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).
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, named by its goal; stays on the current branch
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 the branch, creating it from develop if it is missing
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 -r 1.2.0 # this branch into release/1.2.0
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"] # 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
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 the branch exists neither locally nor on origin, it is created first, the same way as `create -b`, and then checked out.
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 the branch from `origin/develop`, pushes it with upstream tracking, and leaves the current branch checked out. It does not touch the working tree, so it works with uncommitted changes.
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", "auto"],
10
- ["hotfix", "auto"],
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 (!hasNext)
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
- flags.set(name, next);
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 newestContainer(kind) {
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
- let newest;
624
+ const versioned = [];
612
625
  for (const name of candidates) {
613
626
  const version = parseSemver(name.slice(prefix.length));
614
- if (version && (!newest || compare(version, newest.version) > 0))
615
- newest = { name, version };
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.name;
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
- if (!version) {
676
- throw new Error(`usage: gith merge -${kind === "release" ? "r" : "h"} <version>`);
677
- }
678
- target = containerName(kind, version);
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
- if (hasOnOrigin(goal))
686
- throw new Error(`'${goal}' already exists on origin.`);
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", goal, "origin/develop"]);
725
+ gitArgs(["branch", name, "origin/develop"]);
690
726
  } else {
691
- gitArgs(["branch", goal, from]);
727
+ gitArgs(["branch", name, from]);
692
728
  }
693
- gitArgs(["push", "-u", "origin", goal]);
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
- if (!hasLocal(branch) && !hasOnOrigin(branch)) {
1167
- createBranch(branch, isDetached() ? "HEAD" : undefined);
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", "--", branch]);
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, named by its goal (no switch)
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 it from develop if missing (on a detached HEAD: from that commit)
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 pick the branch to merge, then the target (develop, release/*, hotfix/*)
1943
- gith merge [-r <v> | -h <v>] merge this branch into develop, release/<v> or hotfix/<v>
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>] fold staged changes 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)
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
- sync(parseFlags(rest));
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.0",
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": "ISC",
27
+ "license": "MIT",
25
28
  "author": "Vitalii Balabanov",
26
29
  "repository": {
27
30
  "type": "git",