@errhythm/gitmux 1.6.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/README.md +219 -0
- package/bin/gitmux.js +10 -0
- package/package.json +43 -0
- package/src/commands/fetch.js +76 -0
- package/src/commands/mr.js +484 -0
- package/src/commands/portal.js +1711 -0
- package/src/commands/settings.js +490 -0
- package/src/commands/status.js +86 -0
- package/src/commands/switch.js +296 -0
- package/src/config/index.js +22 -0
- package/src/constants.js +15 -0
- package/src/git/branches.js +68 -0
- package/src/git/core.js +67 -0
- package/src/git/templates.js +51 -0
- package/src/gitlab/api.js +46 -0
- package/src/gitlab/helpers.js +73 -0
- package/src/main.js +418 -0
- package/src/ui/colors.js +54 -0
- package/src/ui/print.js +177 -0
- package/src/ui/theme.js +26 -0
- package/src/utils/exec.js +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# gitmux π
|
|
2
|
+
|
|
3
|
+
Multi-repo Git & GitLab workflow CLI β switch branches, manage epics & issues, create MRs, all from the terminal.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
βββββββ βββββββββββ βββββββ βββ βββββββ
|
|
7
|
+
ββββββββ ββββββββββββ βββββββββ βββββββββββ
|
|
8
|
+
βββ ββββββββββββ βββββββ ββββββ ββββββ
|
|
9
|
+
βββ βββββββββββ βββββ βββββββββββββ
|
|
10
|
+
βββββββββββββββββ βββ βββ ββββββββββββββ
|
|
11
|
+
βββββββ ββββββββ βββ βββ βββββ βββββββ
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`gitmux` finds every `.git` repository up to 4 levels deep from your current folder and lets you act on all of them at once β branch switching, status checks, fetch, GitLab merge requests, and a full GitLab development portal (epics, issues, branches) β all from a single interactive TUI.
|
|
15
|
+
|
|
16
|
+
Made by E.R.Rhythm.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Blazing Fast** β Parallel workers (up to CPU count) for all git operations.
|
|
21
|
+
- **Live UI** β Real-time spinners showing `main β develop` transitions per repo.
|
|
22
|
+
- **`gitmux switch`** β Switch branches across all repos simultaneously with pull, stash, create, fuzzy match, and dry-run support.
|
|
23
|
+
- **`gitmux status`** β Table view of all repos: current branch, dirty file count, ahead/behind remote.
|
|
24
|
+
- **`gitmux fetch`** β Fetch all remotes across repos in parallel.
|
|
25
|
+
- **`gitmux mr`** β Interactively create GitLab merge requests via `glab` CLI for one or multiple repos at once.
|
|
26
|
+
- **`gitmux portal`** β GitLab Development Portal: browse assigned Epics, view & create Issues per project, create branches, and checkout primary branches across repos.
|
|
27
|
+
- **`gitmux settings`** β Configure branch suggestion templates, MR defaults, and portal defaults interactively.
|
|
28
|
+
- **Branch suggestions** β Configure interactive switch presets like `sprint/{yyyy}-{mm}-W{w}` in settings.
|
|
29
|
+
- **Auto-stash** β Stash dirty repos before switching, pop after (`--stash`).
|
|
30
|
+
- **Create branch** β Create the branch if it doesn't exist (`--create`).
|
|
31
|
+
- **Fuzzy matching** β Partial branch name resolution with interactive picker for ambiguous matches.
|
|
32
|
+
- **Dry-run mode** β Preview what would happen without touching anything (`--dry-run`).
|
|
33
|
+
- **Smart skipping** β Repos without the target branch are skipped cleanly.
|
|
34
|
+
- **Exclude / filter** β Skip or include repos matching a name pattern.
|
|
35
|
+
- **Color-coded branches** β `main/master` red, `feature/` cyan, `hotfix/` orange, `develop/` purple.
|
|
36
|
+
- **Scriptable** β Pass flags directly for use in CI/bash pipelines.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g @rhythm/gitmux
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
*(Requires Node.js 18+)*
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
### Interactive mode
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
gitmux
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Launches a mode selector: **Switch branches** or **GitLab** (portal + MRs).
|
|
55
|
+
|
|
56
|
+
If configured, interactive switch mode first shows computed branch suggestions from `~/.config/gitmux/gitmux.json`, then offers `Custom branch...` if you want to type something else.
|
|
57
|
+
|
|
58
|
+
### Switch branches
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
gitmux develop
|
|
62
|
+
gitmux main --pull
|
|
63
|
+
gitmux feature/auth --stash --pull
|
|
64
|
+
gitmux feat --fuzzy
|
|
65
|
+
gitmux experiment --create
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Show repo status
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
gitmux status
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Displays a table of every repo with its current branch, dirty file count, and ahead/behind remote sync status.
|
|
75
|
+
|
|
76
|
+
### Fetch all remotes
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
gitmux fetch
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Runs `git fetch --all --prune` across all repos in parallel and shows ahead/behind per repo.
|
|
83
|
+
|
|
84
|
+
### Create merge requests (GitLab)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
gitmux mr
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Requires [`glab`](https://gitlab.com/gitlab-org/cli#installation) to be installed and authenticated.
|
|
91
|
+
|
|
92
|
+
Shows all repos with their current branch. Select one or more repos, fill in the details once (title, description, target branch, labels, draft, push first). `gitmux` builds the `glab mr create` command for each repo and runs them, printing each MR URL on completion.
|
|
93
|
+
|
|
94
|
+
Last-used MR settings (target branch, labels, draft mode, push preference, scope) are remembered between runs.
|
|
95
|
+
|
|
96
|
+
### GitLab Development Portal
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
gitmux portal
|
|
100
|
+
gitmux portal --settings
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Requires [`glab`](https://gitlab.com/gitlab-org/cli#installation) installed and authenticated.
|
|
104
|
+
|
|
105
|
+
Opens an interactive TUI that:
|
|
106
|
+
|
|
107
|
+
1. **Auto-detects your GitLab group** from local repo remote URLs (saved to config on first run).
|
|
108
|
+
2. **Shows Epics assigned to you** β searchable list from the GitLab API.
|
|
109
|
+
3. **Browse issues** under each epic β view branches, set a primary branch per issue.
|
|
110
|
+
4. **Checkout primary branches** β switch all matching local repos to their primary branches in one step.
|
|
111
|
+
5. **Create an Issue** β default title is `Epic Name - Project Name`, pre-fills configured milestone, iteration, and labels.
|
|
112
|
+
6. **Create a Branch** β immediately after issue creation, with a default name of `feature/{iid}-{slug}` cut from the configured base branch.
|
|
113
|
+
|
|
114
|
+
Run with `--settings` to configure portal defaults (see Settings below).
|
|
115
|
+
|
|
116
|
+
### Settings
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
gitmux settings
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Opens an interactive settings menu with three sections:
|
|
123
|
+
|
|
124
|
+
| Section | What you can configure |
|
|
125
|
+
|---------|----------------------|
|
|
126
|
+
| **Portal** | GitLab group path, epic filter label, default milestone, default iteration, default issue labels, base branch |
|
|
127
|
+
| **Switch** | Branch suggestion templates (with date tokens) |
|
|
128
|
+
| **Merge Requests** | Default labels, draft mode, push-before-MR |
|
|
129
|
+
|
|
130
|
+
### Dry-run
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
gitmux --dry-run develop
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Preview what would happen without making any changes.
|
|
137
|
+
|
|
138
|
+
## All flags
|
|
139
|
+
|
|
140
|
+
| Flag | Short | Description |
|
|
141
|
+
|------|-------|-------------|
|
|
142
|
+
| `--pull` | `-p` | Pull latest on the target branch after switching |
|
|
143
|
+
| `--fuzzy` | `-f` | Partial branch name matching |
|
|
144
|
+
| `--create` | `-c` | Create the branch if it doesn't exist |
|
|
145
|
+
| `--stash` | `-s` | Auto-stash dirty repos before switching, pop after |
|
|
146
|
+
| `--fetch` | | Fetch all remotes before switching |
|
|
147
|
+
| `--dry-run` | | Preview actions without executing |
|
|
148
|
+
| `--depth n` | | Repo search depth (default: `4`) |
|
|
149
|
+
| `--exclude p` | | Exclude repos whose name contains pattern `p` |
|
|
150
|
+
| `--filter p` | | Only include repos whose name contains pattern `p` |
|
|
151
|
+
| `--settings` | | Open portal settings (use with `gitmux portal`) |
|
|
152
|
+
| `--version` | `-v` | Show version number |
|
|
153
|
+
| `--help` | `-h` | Show help |
|
|
154
|
+
|
|
155
|
+
## Requirements for `gitmux mr` and `gitmux portal`
|
|
156
|
+
|
|
157
|
+
- [`glab`](https://gitlab.com/gitlab-org/cli#installation) installed and on `$PATH`
|
|
158
|
+
- `glab auth login` completed for your GitLab instance
|
|
159
|
+
|
|
160
|
+
## Configuration
|
|
161
|
+
|
|
162
|
+
`gitmux` reads optional settings from `~/.config/gitmux/gitmux.json`.
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"switch": {
|
|
169
|
+
"branchSuggestions": [
|
|
170
|
+
"sprint/{yyyy}-{mm}-W{w}",
|
|
171
|
+
"release/{yyyy}-{mm}",
|
|
172
|
+
"hotfix/{yyyy}-{mm}-{dd}",
|
|
173
|
+
"main"
|
|
174
|
+
]
|
|
175
|
+
},
|
|
176
|
+
"mr": {
|
|
177
|
+
"scope": "multi",
|
|
178
|
+
"targetBranch": "develop",
|
|
179
|
+
"labels": "backend,sprint",
|
|
180
|
+
"isDraft": false,
|
|
181
|
+
"pushFirst": true
|
|
182
|
+
},
|
|
183
|
+
"portal": {
|
|
184
|
+
"group": "company/subgroup",
|
|
185
|
+
"epicLabelFilter": "TECH::BACKEND",
|
|
186
|
+
"defaultMilestone": { "id": 42, "title": "Sprint 5" },
|
|
187
|
+
"defaultIteration": { "id": "__current__", "title": "Current iteration" },
|
|
188
|
+
"defaultLabels": "backend",
|
|
189
|
+
"defaultBaseBranch": "develop"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Switch template tokens
|
|
195
|
+
|
|
196
|
+
| Token | Description | Example |
|
|
197
|
+
|-------|-------------|---------|
|
|
198
|
+
| `{yyyy}` | 4-digit year | `2026` |
|
|
199
|
+
| `{yy}` | 2-digit year | `26` |
|
|
200
|
+
| `{mm}` | Zero-padded month | `03` |
|
|
201
|
+
| `{m}` | Month | `3` |
|
|
202
|
+
| `{dd}` | Zero-padded day | `07` |
|
|
203
|
+
| `{d}` | Day | `7` |
|
|
204
|
+
| `{q}` | Quarter | `1` |
|
|
205
|
+
| `{w}` | Week of month | `1` |
|
|
206
|
+
| `{ww}` | ISO week of year | `09` |
|
|
207
|
+
|
|
208
|
+
## How it works
|
|
209
|
+
|
|
210
|
+
1. Searches for `.git` folders from the current directory up to `--depth` levels deep.
|
|
211
|
+
2. Applies any `--exclude` / `--filter`, then prints a session info box.
|
|
212
|
+
3. In fuzzy mode, resolves partial branch names per repo (interactive picker for ambiguity).
|
|
213
|
+
4. Runs all git operations concurrently using parallel workers.
|
|
214
|
+
5. Each task shows live `current β target` branch transitions with timing.
|
|
215
|
+
6. Prints a clean summary: done / pulled / stashed / skipped / failed.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
**License**: MIT
|
package/bin/gitmux.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@errhythm/gitmux",
|
|
3
|
+
"version": "1.6.2",
|
|
4
|
+
"description": "Multi-repo Git & GitLab workflow CLI β switch branches, manage epics & issues, create MRs, all from the terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gitmux": "bin/gitmux.js",
|
|
8
|
+
"gmux": "bin/gitmux.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"git",
|
|
19
|
+
"gitlab",
|
|
20
|
+
"branch",
|
|
21
|
+
"multi-repo",
|
|
22
|
+
"polyrepo",
|
|
23
|
+
"monorepo",
|
|
24
|
+
"merge-request",
|
|
25
|
+
"cli",
|
|
26
|
+
"developer-tools",
|
|
27
|
+
"gitlab-cli"
|
|
28
|
+
],
|
|
29
|
+
"author": "rhythm",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"main": "src/main.js",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@inquirer/prompts": "^8.3.0",
|
|
37
|
+
"boxen": "^8.0.1",
|
|
38
|
+
"chalk": "^5.6.2",
|
|
39
|
+
"enquirer": "^2.4.1",
|
|
40
|
+
"figlet": "^1.10.0",
|
|
41
|
+
"listr2": "^10.2.1"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { basename } from "path";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import boxen from "boxen";
|
|
4
|
+
import { Listr } from "listr2";
|
|
5
|
+
|
|
6
|
+
import { getAheadBehind } from "../git/core.js";
|
|
7
|
+
import { execAsync, extractMsg } from "../utils/exec.js";
|
|
8
|
+
import { MAX_JOBS } from "../constants.js";
|
|
9
|
+
import { p } from "../ui/theme.js";
|
|
10
|
+
|
|
11
|
+
export async function cmdFetch(repos) {
|
|
12
|
+
const fetchResults = [];
|
|
13
|
+
const padWidth = String(repos.length).length;
|
|
14
|
+
|
|
15
|
+
const tasks = new Listr(
|
|
16
|
+
repos.map((repo, idx) => {
|
|
17
|
+
const name = basename(repo);
|
|
18
|
+
const idx_ = p.muted(`[${String(idx + 1).padStart(padWidth)}/${repos.length}]`);
|
|
19
|
+
return {
|
|
20
|
+
title: idx_ + " " + chalk.bold(p.white(name)) + p.muted(" fetchingβ¦"),
|
|
21
|
+
task: async (_, task) => {
|
|
22
|
+
const t0 = Date.now();
|
|
23
|
+
try {
|
|
24
|
+
await execAsync("git fetch --all --prune", { cwd: repo });
|
|
25
|
+
const { ahead, behind } = await getAheadBehind(repo);
|
|
26
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
27
|
+
fetchResults.push({ name, ok: true, ahead, behind, elapsed });
|
|
28
|
+
|
|
29
|
+
const syncParts = [];
|
|
30
|
+
if (ahead > 0) syncParts.push(p.cyan(`β${ahead}`));
|
|
31
|
+
if (behind > 0) syncParts.push(p.red(`β${behind}`));
|
|
32
|
+
const syncStr = syncParts.length ? " " + syncParts.join(" ") : "";
|
|
33
|
+
|
|
34
|
+
task.title =
|
|
35
|
+
idx_ + " " + chalk.bold(p.white(name)) +
|
|
36
|
+
" " + p.green("β") +
|
|
37
|
+
" " + p.muted(elapsed + "s") + syncStr;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
const msg = extractMsg(e);
|
|
40
|
+
fetchResults.push({ name, ok: false, msg });
|
|
41
|
+
task.title =
|
|
42
|
+
idx_ + " " + chalk.bold(p.white(name)) +
|
|
43
|
+
" " + p.red("β") +
|
|
44
|
+
" " + p.muted(msg.slice(0, 55));
|
|
45
|
+
throw new Error(msg);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}),
|
|
50
|
+
{ concurrent: MAX_JOBS, exitOnError: false },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
await tasks.run().catch(() => {});
|
|
54
|
+
|
|
55
|
+
const failed = fetchResults.filter((r) => !r.ok).length;
|
|
56
|
+
const avgMs = fetchResults
|
|
57
|
+
.filter((r) => r.ok && r.elapsed)
|
|
58
|
+
.reduce((s, r) => s + parseFloat(r.elapsed), 0) /
|
|
59
|
+
(fetchResults.filter((r) => r.ok).length || 1);
|
|
60
|
+
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(
|
|
63
|
+
boxen(
|
|
64
|
+
(failed === 0
|
|
65
|
+
? chalk.bold(p.green(`β ${repos.length} repos fetched`))
|
|
66
|
+
: p.green(`β ${repos.length - failed} fetched`) + p.slate(" Β· ") + p.red(`β ${failed} failed`)) +
|
|
67
|
+
"\n" + p.muted(`avg ${avgMs.toFixed(1)}s/repo`),
|
|
68
|
+
{
|
|
69
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
70
|
+
borderStyle: "round",
|
|
71
|
+
borderColor: failed > 0 ? "#f87171" : "#4ade80",
|
|
72
|
+
},
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
console.log();
|
|
76
|
+
}
|