@tertium/gith 0.0.0-stage → 0.1.0
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 +15 -0
- package/README.md +203 -2
- package/bin/gith.js +1777 -0
- package/package.json +49 -4
package/bin/gith.js
ADDED
|
@@ -0,0 +1,1777 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { exit } from "node:process";
|
|
5
|
+
|
|
6
|
+
// src/components/args/args.constants.ts
|
|
7
|
+
var SPECS = new Map([
|
|
8
|
+
["branch", "auto"],
|
|
9
|
+
["release", "auto"],
|
|
10
|
+
["hotfix", "auto"],
|
|
11
|
+
["limit", "value"],
|
|
12
|
+
["message", "value"],
|
|
13
|
+
["name", "value"],
|
|
14
|
+
["commit", "value"],
|
|
15
|
+
["file", "value"],
|
|
16
|
+
["change", "value"],
|
|
17
|
+
["regex", "switch"],
|
|
18
|
+
["days", "value"],
|
|
19
|
+
["patch", "switch"],
|
|
20
|
+
["minor", "switch"],
|
|
21
|
+
["major", "switch"],
|
|
22
|
+
["remote", "switch"],
|
|
23
|
+
["local", "switch"],
|
|
24
|
+
["tags", "switch"],
|
|
25
|
+
["commits", "switch"],
|
|
26
|
+
["obsolete", "switch"],
|
|
27
|
+
["graph", "switch"],
|
|
28
|
+
["stashes", "switch"],
|
|
29
|
+
["current-release", "switch"],
|
|
30
|
+
["current-hotfix", "switch"],
|
|
31
|
+
["pockets", "switch"],
|
|
32
|
+
["detached", "switch"],
|
|
33
|
+
["overall", "switch"],
|
|
34
|
+
["current-year", "switch"],
|
|
35
|
+
["current-month", "switch"],
|
|
36
|
+
["current-week", "switch"],
|
|
37
|
+
["last-year", "switch"],
|
|
38
|
+
["last-month", "switch"],
|
|
39
|
+
["last-week", "switch"]
|
|
40
|
+
]);
|
|
41
|
+
var ALIASES = new Map([
|
|
42
|
+
["b", "branch"],
|
|
43
|
+
["r", "release"],
|
|
44
|
+
["h", "hotfix"],
|
|
45
|
+
["l", "limit"],
|
|
46
|
+
["m", "message"],
|
|
47
|
+
["n", "name"]
|
|
48
|
+
]);
|
|
49
|
+
var SHORT = {
|
|
50
|
+
branch: "-b",
|
|
51
|
+
release: "-r",
|
|
52
|
+
hotfix: "-h"
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/components/args/args.utils.ts
|
|
56
|
+
function resolveFlag(token) {
|
|
57
|
+
const match = /^(--?)([a-z][a-z-]*)$/.exec(token);
|
|
58
|
+
if (!match)
|
|
59
|
+
return;
|
|
60
|
+
const word = match[2] ?? "";
|
|
61
|
+
const name = ALIASES.get(word) ?? word;
|
|
62
|
+
if (!SPECS.has(name))
|
|
63
|
+
return;
|
|
64
|
+
return { name, style: match[1] === "--" ? "double" : "single" };
|
|
65
|
+
}
|
|
66
|
+
function spelling(name) {
|
|
67
|
+
if (SPECS.get(name) === "value")
|
|
68
|
+
return `-${name}`;
|
|
69
|
+
return SHORT[name] ?? `--${name}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/components/args/args.ts
|
|
73
|
+
function parseFlags(argv) {
|
|
74
|
+
const flags = new Map;
|
|
75
|
+
for (let i = 0;i < argv.length; i++) {
|
|
76
|
+
const token = argv[i] ?? "";
|
|
77
|
+
const resolved = resolveFlag(token);
|
|
78
|
+
if (!resolved)
|
|
79
|
+
throw new Error(`unknown option '${token}'.`);
|
|
80
|
+
const { name, style } = resolved;
|
|
81
|
+
const kind = SPECS.get(name) ?? "switch";
|
|
82
|
+
if (flags.has(name))
|
|
83
|
+
throw new Error(`'${name}' given more than once.`);
|
|
84
|
+
const next = argv[i + 1];
|
|
85
|
+
const hasNext = next !== undefined && !next.startsWith("-");
|
|
86
|
+
if (style === "single") {
|
|
87
|
+
if (kind === "switch")
|
|
88
|
+
throw new Error(`${token} is a switch; write --${name}.`);
|
|
89
|
+
if (!hasNext)
|
|
90
|
+
throw new Error(`${token} needs a value.`);
|
|
91
|
+
flags.set(name, next);
|
|
92
|
+
i++;
|
|
93
|
+
} else if (kind === "switch") {
|
|
94
|
+
flags.set(name, "true");
|
|
95
|
+
} else if (kind === "value") {
|
|
96
|
+
throw new Error(`${token} takes a value; write -${name} <value>.`);
|
|
97
|
+
} else {
|
|
98
|
+
if (hasNext)
|
|
99
|
+
throw new Error(`${token} takes no value. For an explicit value use -${name} <value>.`);
|
|
100
|
+
flags.set(name, "");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return flags;
|
|
104
|
+
}
|
|
105
|
+
function parseLimit(flags) {
|
|
106
|
+
const raw = flags.get("limit");
|
|
107
|
+
if (raw === undefined)
|
|
108
|
+
return;
|
|
109
|
+
if (!/^[1-9]\d*$/.test(raw))
|
|
110
|
+
throw new Error("-limit needs a positive whole number.");
|
|
111
|
+
return Number(raw);
|
|
112
|
+
}
|
|
113
|
+
function onlyOne(flags, names) {
|
|
114
|
+
const present = names.filter((name) => flags.has(name));
|
|
115
|
+
if (present.length > 1) {
|
|
116
|
+
throw new Error(`use only one of: ${names.map(spelling).join(", ")}.`);
|
|
117
|
+
}
|
|
118
|
+
return present[0];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/components/git/git.ts
|
|
122
|
+
import { execFileSync } from "node:child_process";
|
|
123
|
+
var verbose = false;
|
|
124
|
+
function setVerbose(on) {
|
|
125
|
+
verbose = on;
|
|
126
|
+
}
|
|
127
|
+
var dryRun = false;
|
|
128
|
+
function setDryRun(on) {
|
|
129
|
+
dryRun = on;
|
|
130
|
+
}
|
|
131
|
+
function isDryRun() {
|
|
132
|
+
return dryRun;
|
|
133
|
+
}
|
|
134
|
+
function git(args) {
|
|
135
|
+
act(args.split(" "));
|
|
136
|
+
}
|
|
137
|
+
function gitArgs(args, options = {}) {
|
|
138
|
+
act(args, options);
|
|
139
|
+
}
|
|
140
|
+
function captureArgs(args) {
|
|
141
|
+
return run(args);
|
|
142
|
+
}
|
|
143
|
+
function hasLocal(branch) {
|
|
144
|
+
return captureArgs(["branch", "--list", branch]) !== "";
|
|
145
|
+
}
|
|
146
|
+
function assertBranchName(name) {
|
|
147
|
+
try {
|
|
148
|
+
captureArgs(["check-ref-format", "--branch", name]);
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error(`'${name}' is not a valid branch name.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function capture(args) {
|
|
154
|
+
return run(args.split(" "));
|
|
155
|
+
}
|
|
156
|
+
function hasOnOrigin(branch) {
|
|
157
|
+
return capture(`ls-remote --heads origin ${branch}`) !== "";
|
|
158
|
+
}
|
|
159
|
+
function listTags() {
|
|
160
|
+
const local = capture("tag --list").split(`
|
|
161
|
+
`);
|
|
162
|
+
const remote = capture("ls-remote --tags origin").split(`
|
|
163
|
+
`).map((line) => line.split("\t")[1] ?? "").map((ref) => ref.replace(/^refs\/tags\//, "").replace(/\^\{\}$/, ""));
|
|
164
|
+
return [...local, ...remote].filter(Boolean);
|
|
165
|
+
}
|
|
166
|
+
function assertCleanTree() {
|
|
167
|
+
try {
|
|
168
|
+
captureArgs(["update-index", "-q", "--refresh"]);
|
|
169
|
+
} catch {}
|
|
170
|
+
const changed = captureArgs(["status", "--porcelain", "--untracked-files=no"]).split(`
|
|
171
|
+
`).filter(Boolean);
|
|
172
|
+
if (changed.length === 0)
|
|
173
|
+
return;
|
|
174
|
+
const shown = changed.slice(0, 10).map((line) => ` ${line}`).join(`
|
|
175
|
+
`);
|
|
176
|
+
const more = changed.length > 10 ? `
|
|
177
|
+
... and ${changed.length - 10} more` : "";
|
|
178
|
+
throw new Error(`uncommitted changes to tracked files:
|
|
179
|
+
${shown}${more}
|
|
180
|
+
commit them, or stash them with gith stash.`);
|
|
181
|
+
}
|
|
182
|
+
function isDetached() {
|
|
183
|
+
try {
|
|
184
|
+
captureArgs(["symbolic-ref", "-q", "HEAD"]);
|
|
185
|
+
return false;
|
|
186
|
+
} catch {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function assertSynced(branches) {
|
|
191
|
+
if (dryRun) {
|
|
192
|
+
console.log("dry run: sync check skipped");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
git("fetch origin");
|
|
196
|
+
for (const branch of branches) {
|
|
197
|
+
if (capture(`rev-parse ${branch}`) !== capture(`rev-parse origin/${branch}`)) {
|
|
198
|
+
throw new Error(`'${branch}' is not in sync with origin/${branch}.`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function show(args) {
|
|
203
|
+
const shown = args.map((arg) => /\s/.test(arg) ? `"${arg}"` : arg);
|
|
204
|
+
return `> git ${shown.join(" ")}`;
|
|
205
|
+
}
|
|
206
|
+
function run(args) {
|
|
207
|
+
if (verbose)
|
|
208
|
+
console.log(show(args));
|
|
209
|
+
return execFileSync("git", args, { encoding: "utf-8" }).trim();
|
|
210
|
+
}
|
|
211
|
+
function act(args, options = {}) {
|
|
212
|
+
console.log(show(args));
|
|
213
|
+
if (dryRun)
|
|
214
|
+
return;
|
|
215
|
+
execFileSync("git", args, { stdio: "inherit", ...options });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/components/doctor/doctor.utils.ts
|
|
219
|
+
function check(name, test, failure = "fail") {
|
|
220
|
+
return {
|
|
221
|
+
name,
|
|
222
|
+
run: () => {
|
|
223
|
+
try {
|
|
224
|
+
const problem = test();
|
|
225
|
+
return problem === undefined ? { level: "ok", detail: "" } : { level: failure, detail: problem };
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return {
|
|
228
|
+
level: failure,
|
|
229
|
+
detail: error instanceof Error ? error.message.split(`
|
|
230
|
+
`)[0] ?? "" : String(error)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/components/doctor/doctor.constants.ts
|
|
238
|
+
var CHECKS = [
|
|
239
|
+
check("git is installed", () => {
|
|
240
|
+
captureArgs(["--version"]);
|
|
241
|
+
return;
|
|
242
|
+
}),
|
|
243
|
+
check("inside a git repository", () => {
|
|
244
|
+
captureArgs(["rev-parse", "--is-inside-work-tree"]);
|
|
245
|
+
return;
|
|
246
|
+
}),
|
|
247
|
+
check("origin is configured", () => {
|
|
248
|
+
captureArgs(["remote", "get-url", "origin"]);
|
|
249
|
+
return;
|
|
250
|
+
}),
|
|
251
|
+
check("origin is reachable", () => {
|
|
252
|
+
captureArgs(["ls-remote", "--heads", "origin"]);
|
|
253
|
+
return;
|
|
254
|
+
}),
|
|
255
|
+
check("main exists on origin", () => hasOnOrigin("main") ? undefined : "missing"),
|
|
256
|
+
check("develop exists on origin", () => hasOnOrigin("develop") ? undefined : "missing: create it before work branches"),
|
|
257
|
+
check("working tree is clean", () => captureArgs(["status", "--porcelain"]) === "" ? undefined : "uncommitted changes", "warn")
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
// src/components/doctor/doctor.ts
|
|
261
|
+
function doctor(flags) {
|
|
262
|
+
if (flags.size > 0)
|
|
263
|
+
throw new Error("usage: gith doctor");
|
|
264
|
+
let ok = true;
|
|
265
|
+
for (const item of CHECKS) {
|
|
266
|
+
const { level, detail } = item.run();
|
|
267
|
+
if (level === "fail")
|
|
268
|
+
ok = false;
|
|
269
|
+
const mark = level === "ok" ? "✔" : level === "warn" ? "!" : "✖";
|
|
270
|
+
console.log(`${mark} ${item.name}${detail ? `: ${detail}` : ""}`);
|
|
271
|
+
}
|
|
272
|
+
console.log(ok ? "ready" : "not ready: fix the ✖ items above");
|
|
273
|
+
return ok;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/components/find/find.constants.ts
|
|
277
|
+
var DEFAULT_LIMIT = 5;
|
|
278
|
+
var FILTERS = ["commit", "file", "change"];
|
|
279
|
+
|
|
280
|
+
// src/components/find/find.utils.ts
|
|
281
|
+
function describe(message, file, change, regex = false) {
|
|
282
|
+
const parts = [];
|
|
283
|
+
if (message !== undefined)
|
|
284
|
+
parts.push(`message "${message}"`);
|
|
285
|
+
if (file !== undefined)
|
|
286
|
+
parts.push(`file "${file}"`);
|
|
287
|
+
if (change !== undefined)
|
|
288
|
+
parts.push(`${regex ? "regex" : "change"} "${change}"`);
|
|
289
|
+
return parts.join(" and ");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/components/find/find.ts
|
|
293
|
+
function find(flags) {
|
|
294
|
+
const message = flags.get("commit");
|
|
295
|
+
const file = flags.get("file");
|
|
296
|
+
const change = flags.get("change");
|
|
297
|
+
const extra = [...flags.keys()].filter((key) => key !== "limit" && key !== "regex" && !FILTERS.includes(key));
|
|
298
|
+
const usage = 'usage: gith find -commit "<text>" | -file "<pattern>" | -change "<text>" [--regex] [-limit <n>]';
|
|
299
|
+
if (message === undefined && file === undefined && change === undefined || extra.length > 0) {
|
|
300
|
+
throw new Error(usage);
|
|
301
|
+
}
|
|
302
|
+
if (flags.has("regex") && change === undefined) {
|
|
303
|
+
throw new Error("--regex only applies to -change.");
|
|
304
|
+
}
|
|
305
|
+
const limit = parseLimit(flags) ?? DEFAULT_LIMIT;
|
|
306
|
+
const args = [
|
|
307
|
+
"log",
|
|
308
|
+
`-n${limit}`,
|
|
309
|
+
"--date=short",
|
|
310
|
+
"--format=%x1e%h %ad %an %s"
|
|
311
|
+
];
|
|
312
|
+
if (message !== undefined) {
|
|
313
|
+
args.push("--fixed-strings", "--regexp-ignore-case", `--grep=${message}`);
|
|
314
|
+
}
|
|
315
|
+
if (change !== undefined) {
|
|
316
|
+
args.push(flags.has("regex") ? `-G${change}` : `-S${change}`);
|
|
317
|
+
}
|
|
318
|
+
if (file !== undefined) {
|
|
319
|
+
args.push("--name-only", "--", file);
|
|
320
|
+
}
|
|
321
|
+
const records = captureArgs(args).split("\x1E").map((record) => record.trim()).filter(Boolean);
|
|
322
|
+
if (records.length === 0) {
|
|
323
|
+
console.log(`No commits match ${describe(message, file, change, flags.has("regex"))}.`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
console.log(records.map((record) => {
|
|
327
|
+
const [header = "", ...paths] = record.split(`
|
|
328
|
+
`);
|
|
329
|
+
return [
|
|
330
|
+
header,
|
|
331
|
+
...paths.filter(Boolean).map((path) => ` ${path.trim()}`)
|
|
332
|
+
].join(`
|
|
333
|
+
`);
|
|
334
|
+
}).join(`
|
|
335
|
+
`));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/components/rules/rules.constants.ts
|
|
339
|
+
var CONTAINER = /^(develop|main|release\/.+|hotfix\/.+)$/;
|
|
340
|
+
var TARGET = /^(develop|release\/.+|hotfix\/.+)$/;
|
|
341
|
+
|
|
342
|
+
// src/components/rules/rules.ts
|
|
343
|
+
function isContainer(name) {
|
|
344
|
+
return CONTAINER.test(name);
|
|
345
|
+
}
|
|
346
|
+
function isTarget(name) {
|
|
347
|
+
return TARGET.test(name);
|
|
348
|
+
}
|
|
349
|
+
function containerName(kind, version) {
|
|
350
|
+
return `${kind}/${version}`;
|
|
351
|
+
}
|
|
352
|
+
function containerBase(kind) {
|
|
353
|
+
return kind === "release" ? "develop" : "main";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/components/versions/versions.constants.ts
|
|
357
|
+
var SEMVER = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
|
358
|
+
|
|
359
|
+
// src/components/versions/versions.ts
|
|
360
|
+
function parseSemver(tag) {
|
|
361
|
+
const match = SEMVER.exec(tag.trim());
|
|
362
|
+
if (!match)
|
|
363
|
+
return;
|
|
364
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
365
|
+
}
|
|
366
|
+
function compare(a, b) {
|
|
367
|
+
for (let i = 0;i < 3; i++) {
|
|
368
|
+
const diff = (a[i] ?? 0) - (b[i] ?? 0);
|
|
369
|
+
if (diff !== 0)
|
|
370
|
+
return diff;
|
|
371
|
+
}
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
function latestVersion(tags) {
|
|
375
|
+
let latest;
|
|
376
|
+
for (const tag of tags) {
|
|
377
|
+
const version = parseSemver(tag);
|
|
378
|
+
if (version && (!latest || compare(version, latest) > 0))
|
|
379
|
+
latest = version;
|
|
380
|
+
}
|
|
381
|
+
return latest;
|
|
382
|
+
}
|
|
383
|
+
function nextVersion(tags, bump) {
|
|
384
|
+
const [major, minor, patch] = latestVersion(tags) ?? [0, 0, 0];
|
|
385
|
+
return bump === "minor" ? `${major}.${minor + 1}.0` : `${major}.${minor}.${patch + 1}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/components/flow/flow.utils.ts
|
|
389
|
+
function newestContainer(kind) {
|
|
390
|
+
const prefix = `${kind}/`;
|
|
391
|
+
const local = captureArgs([
|
|
392
|
+
"for-each-ref",
|
|
393
|
+
"--format=%(refname:short)",
|
|
394
|
+
"refs/heads"
|
|
395
|
+
]).split(`
|
|
396
|
+
`);
|
|
397
|
+
const origin = captureArgs(["ls-remote", "--heads", "origin"]).split(`
|
|
398
|
+
`).map((line) => (line.split("\t")[1] ?? "").replace(/^refs\/heads\//, ""));
|
|
399
|
+
const candidates = new Set([...local, ...origin].filter((name) => name.startsWith(prefix)));
|
|
400
|
+
let newest;
|
|
401
|
+
for (const name of candidates) {
|
|
402
|
+
const version = parseSemver(name.slice(prefix.length));
|
|
403
|
+
if (version && (!newest || compare(version, newest.version) > 0))
|
|
404
|
+
newest = { name, version };
|
|
405
|
+
}
|
|
406
|
+
if (!newest)
|
|
407
|
+
throw new Error(`no ${kind} branch exists, locally or on origin.`);
|
|
408
|
+
return newest.name;
|
|
409
|
+
}
|
|
410
|
+
function nextContainerVersion(kind) {
|
|
411
|
+
const bump = kind === "release" ? "minor" : "patch";
|
|
412
|
+
const tags = listTags();
|
|
413
|
+
let version = nextVersion(tags, bump);
|
|
414
|
+
while (hasOnOrigin(containerName(kind, version))) {
|
|
415
|
+
tags.push(version);
|
|
416
|
+
version = nextVersion(tags, bump);
|
|
417
|
+
}
|
|
418
|
+
console.log(`Next ${kind} version from existing tags: ${version}`);
|
|
419
|
+
return version;
|
|
420
|
+
}
|
|
421
|
+
function open(kind, version) {
|
|
422
|
+
const name = containerName(kind, version);
|
|
423
|
+
if (hasOnOrigin(name))
|
|
424
|
+
throw new Error(`'${name}' already exists on origin.`);
|
|
425
|
+
const base = containerBase(kind);
|
|
426
|
+
gitArgs(["fetch", "origin", base]);
|
|
427
|
+
gitArgs(["branch", name, `origin/${base}`]);
|
|
428
|
+
gitArgs(["push", "-u", "origin", name]);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/components/flow/flow.ts
|
|
432
|
+
function create(flags) {
|
|
433
|
+
const kind = onlyOne(flags, ["branch", "release", "hotfix"]);
|
|
434
|
+
if (!kind)
|
|
435
|
+
throw new Error("usage: gith create -b <branch> | -r [version] | -h [version]");
|
|
436
|
+
const value = flags.get(kind) ?? "";
|
|
437
|
+
if (kind === "branch") {
|
|
438
|
+
if (!value)
|
|
439
|
+
throw new Error("usage: gith create -b <branch>");
|
|
440
|
+
createBranch(value);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
open(kind, value || nextContainerVersion(kind));
|
|
444
|
+
}
|
|
445
|
+
function merge(flags) {
|
|
446
|
+
const kind = onlyOne(flags, [
|
|
447
|
+
"release",
|
|
448
|
+
"hotfix",
|
|
449
|
+
"current-release",
|
|
450
|
+
"current-hotfix"
|
|
451
|
+
]);
|
|
452
|
+
let target = "develop";
|
|
453
|
+
if (kind === "current-release")
|
|
454
|
+
target = newestContainer("release");
|
|
455
|
+
else if (kind === "current-hotfix")
|
|
456
|
+
target = newestContainer("hotfix");
|
|
457
|
+
else if (kind) {
|
|
458
|
+
const version = flags.get(kind) ?? "";
|
|
459
|
+
if (!version) {
|
|
460
|
+
throw new Error(`usage: gith merge -${kind === "release" ? "r" : "h"} <version>`);
|
|
461
|
+
}
|
|
462
|
+
target = containerName(kind, version);
|
|
463
|
+
}
|
|
464
|
+
mergeInto(target);
|
|
465
|
+
}
|
|
466
|
+
function createBranch(goal, from) {
|
|
467
|
+
if (isContainer(goal))
|
|
468
|
+
throw new Error(`'${goal}' is a container name; pick a goal.`);
|
|
469
|
+
if (hasOnOrigin(goal))
|
|
470
|
+
throw new Error(`'${goal}' already exists on origin.`);
|
|
471
|
+
if (from === undefined) {
|
|
472
|
+
gitArgs(["fetch", "origin", "develop"]);
|
|
473
|
+
gitArgs(["branch", goal, "origin/develop"]);
|
|
474
|
+
} else {
|
|
475
|
+
gitArgs(["branch", goal, from]);
|
|
476
|
+
}
|
|
477
|
+
gitArgs(["push", "-u", "origin", goal]);
|
|
478
|
+
}
|
|
479
|
+
function mergeInto(target) {
|
|
480
|
+
if (!isTarget(target))
|
|
481
|
+
throw new Error(`'${target}' is not a valid target.`);
|
|
482
|
+
const source = capture("rev-parse --abbrev-ref HEAD");
|
|
483
|
+
if (isContainer(source))
|
|
484
|
+
throw new Error(`'${source}' is a container and cannot be merged from.`);
|
|
485
|
+
if (source === target)
|
|
486
|
+
throw new Error(`already on '${target}', nothing to merge.`);
|
|
487
|
+
if (!hasOnOrigin(target))
|
|
488
|
+
throw new Error(`'${target}' does not exist on origin.`);
|
|
489
|
+
assertCleanTree();
|
|
490
|
+
if (hasOnOrigin(source))
|
|
491
|
+
git(`pull origin ${source}`);
|
|
492
|
+
git(`push -u origin ${source}`);
|
|
493
|
+
git(`checkout ${target}`);
|
|
494
|
+
git("pull");
|
|
495
|
+
git(`checkout ${source}`);
|
|
496
|
+
try {
|
|
497
|
+
gitArgs(["merge", target, "--no-edit"]);
|
|
498
|
+
} catch {
|
|
499
|
+
gitArgs(["merge", "--abort"]);
|
|
500
|
+
throw new Error(`merging '${target}' into '${source}' conflicts; resolve on '${source}' and retry.`);
|
|
501
|
+
}
|
|
502
|
+
git(`push origin ${source}`);
|
|
503
|
+
git(`checkout ${target}`);
|
|
504
|
+
git(`merge --no-ff ${source} --no-edit`);
|
|
505
|
+
git(`push origin ${target}`);
|
|
506
|
+
assertSynced([target, source]);
|
|
507
|
+
console.log(`
|
|
508
|
+
✅ '${source}' merged into '${target}', both in sync with origin.`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/components/prompt/prompt.ts
|
|
512
|
+
import { createInterface } from "node:readline/promises";
|
|
513
|
+
var assumeYes = false;
|
|
514
|
+
function setAssumeYes(on) {
|
|
515
|
+
assumeYes = on;
|
|
516
|
+
}
|
|
517
|
+
async function confirm(question) {
|
|
518
|
+
if (isDryRun()) {
|
|
519
|
+
console.log(`dry run: would ask: ${question}`);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
if (assumeYes)
|
|
523
|
+
return true;
|
|
524
|
+
if (!process.stdin.isTTY)
|
|
525
|
+
throw new Error("this needs a terminal to confirm; pass --yes to skip the question.");
|
|
526
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
527
|
+
try {
|
|
528
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
529
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
530
|
+
} finally {
|
|
531
|
+
rl.close();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function ask(question) {
|
|
535
|
+
if (isDryRun()) {
|
|
536
|
+
console.log(`dry run: would ask: ${question}`);
|
|
537
|
+
return "<message>";
|
|
538
|
+
}
|
|
539
|
+
if (!process.stdin.isTTY)
|
|
540
|
+
throw new Error("needs a terminal to ask; pass -m <message>.");
|
|
541
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
542
|
+
try {
|
|
543
|
+
return (await rl.question(question)).trim();
|
|
544
|
+
} finally {
|
|
545
|
+
rl.close();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/components/history/history.utils.ts
|
|
550
|
+
function noFlags(flags, command) {
|
|
551
|
+
if (flags.size > 0)
|
|
552
|
+
throw new Error(`usage: gith ${command}`);
|
|
553
|
+
}
|
|
554
|
+
function currentBranch() {
|
|
555
|
+
const name = capture("rev-parse --abbrev-ref HEAD");
|
|
556
|
+
if (name === "HEAD")
|
|
557
|
+
throw new Error("HEAD is detached; check out a branch first.");
|
|
558
|
+
return name;
|
|
559
|
+
}
|
|
560
|
+
function assertNotPushed() {
|
|
561
|
+
if (captureArgs(["branch", "-r", "--contains", "HEAD"]) !== "") {
|
|
562
|
+
throw new Error("HEAD is already on origin; changing it would need a force-push.");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function remoteSha(branch) {
|
|
566
|
+
return captureArgs(["ls-remote", "--heads", "origin", branch]).split("\t")[0] ?? "";
|
|
567
|
+
}
|
|
568
|
+
async function askMessage() {
|
|
569
|
+
const text = await ask("Commit message: ");
|
|
570
|
+
if (text === "")
|
|
571
|
+
throw new Error("a commit needs a message; nothing was committed.");
|
|
572
|
+
return text;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/components/history/history.ts
|
|
576
|
+
function update(flags) {
|
|
577
|
+
noFlags(flags, "update");
|
|
578
|
+
const branch = currentBranch();
|
|
579
|
+
if (isContainer(branch))
|
|
580
|
+
throw new Error(`'${branch}' is a container; update works on a work branch.`);
|
|
581
|
+
assertCleanTree();
|
|
582
|
+
gitArgs(["fetch", "origin", "develop"]);
|
|
583
|
+
try {
|
|
584
|
+
gitArgs(["merge", "origin/develop", "--no-edit"]);
|
|
585
|
+
} catch {
|
|
586
|
+
if (isDryRun())
|
|
587
|
+
return;
|
|
588
|
+
gitArgs(["merge", "--abort"]);
|
|
589
|
+
throw new Error("merging origin/develop conflicts; the merge was aborted. Resolve it by hand, then retry.");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function undo(flags) {
|
|
593
|
+
noFlags(flags, "undo");
|
|
594
|
+
currentBranch();
|
|
595
|
+
assertCleanTree();
|
|
596
|
+
assertNotPushed();
|
|
597
|
+
gitArgs(["reset", "--soft", "HEAD~1"]);
|
|
598
|
+
}
|
|
599
|
+
async function save(flags) {
|
|
600
|
+
const message = flags.get("message");
|
|
601
|
+
const extra = [...flags.keys()].filter((key) => key !== "message");
|
|
602
|
+
if (extra.length > 0)
|
|
603
|
+
throw new Error("usage: gith save [-m <message>]");
|
|
604
|
+
const branch = currentBranch();
|
|
605
|
+
if (captureArgs(["diff", "--cached", "--name-only"]) === "") {
|
|
606
|
+
throw new Error("nothing staged; stage the changes to add to the last commit.");
|
|
607
|
+
}
|
|
608
|
+
const owned = hasOwnCommit();
|
|
609
|
+
const pushed = captureArgs(["branch", "-r", "--contains", "HEAD"]) !== "";
|
|
610
|
+
const rewrite = owned && pushed;
|
|
611
|
+
const remote = remoteSha(branch);
|
|
612
|
+
if (rewrite && remote === "") {
|
|
613
|
+
throw new Error("HEAD is already on origin under another branch; changing it would need a force-push.");
|
|
614
|
+
}
|
|
615
|
+
if (rewrite) {
|
|
616
|
+
const yes = await confirm(`HEAD is already on origin/${branch}. Save it and force-push ${branch}? [Y/n] `);
|
|
617
|
+
if (!yes) {
|
|
618
|
+
console.log("save cancelled; nothing changed.");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (owned) {
|
|
623
|
+
gitArgs(message ? ["commit", "--amend", "-m", message] : ["commit", "--amend", "--no-edit"]);
|
|
624
|
+
} else {
|
|
625
|
+
const text = message ?? await askMessage();
|
|
626
|
+
gitArgs(["commit", "-m", text]);
|
|
627
|
+
}
|
|
628
|
+
if (remote === "") {
|
|
629
|
+
console.log(`origin has no ${branch}; not pushed.`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (!isDryRun() && captureArgs(["rev-parse", "HEAD"]) === remote) {
|
|
633
|
+
console.log("origin already has this commit; not pushed.");
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (rewrite)
|
|
637
|
+
gitArgs(["push", "--force-with-lease", "origin", branch]);
|
|
638
|
+
else
|
|
639
|
+
gitArgs(["push", "origin", branch]);
|
|
640
|
+
}
|
|
641
|
+
async function squash(flags) {
|
|
642
|
+
const count = parseLimit(flags);
|
|
643
|
+
const message = flags.get("message");
|
|
644
|
+
const extra = [...flags.keys()].filter((key) => key !== "limit" && key !== "message");
|
|
645
|
+
if (count === undefined || extra.length > 0) {
|
|
646
|
+
throw new Error("usage: gith squash -limit <n> [-m <message>]");
|
|
647
|
+
}
|
|
648
|
+
if (count < 2)
|
|
649
|
+
throw new Error("-limit needs at least 2 commits to squash.");
|
|
650
|
+
const branch = currentBranch();
|
|
651
|
+
if (isContainer(branch))
|
|
652
|
+
throw new Error(`'${branch}' is a container; squash works on a work branch.`);
|
|
653
|
+
assertCleanTree();
|
|
654
|
+
const total = Number(captureArgs(["rev-list", "--count", "HEAD"]));
|
|
655
|
+
if (count >= total) {
|
|
656
|
+
throw new Error(`only ${total} commits on this branch; squashing ${count} would reach the first commit.`);
|
|
657
|
+
}
|
|
658
|
+
const commits = captureArgs(["rev-list", `-n${count}`, "HEAD"]).split(`
|
|
659
|
+
`);
|
|
660
|
+
const oldest = commits[count - 1] ?? "";
|
|
661
|
+
if (captureArgs(["rev-list", "--merges", `-n${count}`, "HEAD"]) !== "") {
|
|
662
|
+
throw new Error("the commits include a merge; squashing would flatten it.");
|
|
663
|
+
}
|
|
664
|
+
if (captureArgs(["branch", "-r", "--contains", oldest]) !== "") {
|
|
665
|
+
throw new Error("these commits are already on origin; squashing them would need a force-push.");
|
|
666
|
+
}
|
|
667
|
+
console.log("Will combine:");
|
|
668
|
+
for (const line of captureArgs(["log", `-n${count}`, "--oneline"]).split(`
|
|
669
|
+
`)) {
|
|
670
|
+
console.log(` ${line}`);
|
|
671
|
+
}
|
|
672
|
+
if (!await confirm(`Combine these ${count} commits into one? [Y/n] `)) {
|
|
673
|
+
console.log("Nothing changed.");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
gitArgs(["reset", "--soft", `HEAD~${count}`]);
|
|
677
|
+
if (message)
|
|
678
|
+
gitArgs(["commit", "-m", message]);
|
|
679
|
+
else
|
|
680
|
+
gitArgs(["commit", "-C", oldest]);
|
|
681
|
+
}
|
|
682
|
+
function hasOwnCommit() {
|
|
683
|
+
for (const base of ["origin/develop", "origin/main"]) {
|
|
684
|
+
try {
|
|
685
|
+
return captureArgs(["rev-list", "--count", `${base}..HEAD`]) !== "0";
|
|
686
|
+
} catch {}
|
|
687
|
+
}
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/components/list/list.constants.ts
|
|
692
|
+
var DEFAULT_LIMIT2 = 5;
|
|
693
|
+
var GORLUM = [
|
|
694
|
+
' .-"""-.',
|
|
695
|
+
" / o o \\",
|
|
696
|
+
" | ___ | my precious pockets!",
|
|
697
|
+
" \\ (___) /",
|
|
698
|
+
" '-._.-'"
|
|
699
|
+
];
|
|
700
|
+
|
|
701
|
+
// src/components/list/list.utils.ts
|
|
702
|
+
function yesNo(value) {
|
|
703
|
+
return value ? "yes" : "no";
|
|
704
|
+
}
|
|
705
|
+
function commits(limit, graph) {
|
|
706
|
+
if (!hasHead()) {
|
|
707
|
+
console.log("No commits.");
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const shape = graph ? ["--graph", "--oneline", "--decorate"] : ["--date=short", "--format=%h %ad %an %s"];
|
|
711
|
+
console.log(captureArgs(["log", `-n${limit}`, ...shape]));
|
|
712
|
+
}
|
|
713
|
+
function hasHead() {
|
|
714
|
+
try {
|
|
715
|
+
captureArgs(["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
716
|
+
return true;
|
|
717
|
+
} catch {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/components/list/list.ts
|
|
723
|
+
function tagPairs(local, origin) {
|
|
724
|
+
const names = [...new Set([...local, ...origin])];
|
|
725
|
+
return names.map((name) => ({
|
|
726
|
+
name,
|
|
727
|
+
local: local.includes(name),
|
|
728
|
+
origin: origin.includes(name)
|
|
729
|
+
})).sort((a, b) => {
|
|
730
|
+
const va = parseSemver(a.name);
|
|
731
|
+
const vb = parseSemver(b.name);
|
|
732
|
+
if (va && vb)
|
|
733
|
+
return compare(vb, va);
|
|
734
|
+
if (va)
|
|
735
|
+
return -1;
|
|
736
|
+
if (vb)
|
|
737
|
+
return 1;
|
|
738
|
+
return a.name.localeCompare(b.name);
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
function list(flags) {
|
|
742
|
+
const kind = onlyOne(flags, ["tags", "commits", "stashes", "pockets"]);
|
|
743
|
+
if (!kind)
|
|
744
|
+
throw new Error("usage: gith list --tags | --commits [--graph] | --stashes | --pockets [-limit <n>]");
|
|
745
|
+
if (flags.has("graph") && kind !== "commits")
|
|
746
|
+
throw new Error("--graph only applies to --commits.");
|
|
747
|
+
const limit = parseLimit(flags) ?? DEFAULT_LIMIT2;
|
|
748
|
+
if (kind === "commits")
|
|
749
|
+
commits(limit, flags.has("graph"));
|
|
750
|
+
else if (kind === "stashes")
|
|
751
|
+
stashes(limit, false);
|
|
752
|
+
else if (kind === "pockets")
|
|
753
|
+
stashes(limit, true);
|
|
754
|
+
else
|
|
755
|
+
tags(limit);
|
|
756
|
+
}
|
|
757
|
+
function localTags() {
|
|
758
|
+
return captureArgs(["tag", "--list"]).split(`
|
|
759
|
+
`).filter(Boolean);
|
|
760
|
+
}
|
|
761
|
+
function stashes(limit, withGorlum) {
|
|
762
|
+
if (withGorlum)
|
|
763
|
+
console.log(GORLUM.join(`
|
|
764
|
+
`));
|
|
765
|
+
const rows = captureArgs([
|
|
766
|
+
"stash",
|
|
767
|
+
"list",
|
|
768
|
+
"--date=short",
|
|
769
|
+
"--format=%gd %cd %gs"
|
|
770
|
+
]).split(`
|
|
771
|
+
`).filter(Boolean);
|
|
772
|
+
if (rows.length === 0) {
|
|
773
|
+
console.log("No stashes.");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const shown = rows.slice(0, limit);
|
|
777
|
+
console.log(shown.join(`
|
|
778
|
+
`));
|
|
779
|
+
if (shown.length < rows.length) {
|
|
780
|
+
console.log(`showing ${shown.length} of ${rows.length}; -limit <n> shows more`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function tags(limit) {
|
|
784
|
+
const pairs = tagPairs(localTags(), originTags());
|
|
785
|
+
if (pairs.length === 0) {
|
|
786
|
+
console.log("No tags.");
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const shown = pairs.slice(0, limit);
|
|
790
|
+
const width = Math.max("TAG".length, ...pairs.map((pair) => pair.name.length));
|
|
791
|
+
console.log(`${"TAG".padEnd(width)} LOCAL ORIGIN`);
|
|
792
|
+
for (const pair of shown) {
|
|
793
|
+
console.log(`${pair.name.padEnd(width)} ${yesNo(pair.local).padEnd(5)} ${yesNo(pair.origin)}`);
|
|
794
|
+
}
|
|
795
|
+
if (shown.length < pairs.length) {
|
|
796
|
+
console.log(`showing ${shown.length} of ${pairs.length}; -limit <n> shows more`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function originTags() {
|
|
800
|
+
return captureArgs(["ls-remote", "--tags", "origin"]).split(`
|
|
801
|
+
`).filter(Boolean).map((line) => (line.split("\t")[1] ?? "").replace(/^refs\/tags\//, "")).filter((ref) => ref && !ref.endsWith("^{}"));
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/components/maintenance/maintenance.ts
|
|
805
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
806
|
+
|
|
807
|
+
// src/components/maintenance/maintenance.constants.ts
|
|
808
|
+
var DEFAULT_DAYS = 90;
|
|
809
|
+
|
|
810
|
+
// src/components/maintenance/maintenance.utils.ts
|
|
811
|
+
function noFlags2(flags, command) {
|
|
812
|
+
if (flags.size > 0)
|
|
813
|
+
throw new Error(`usage: gith ${command}`);
|
|
814
|
+
}
|
|
815
|
+
function requireBranch(flags, command) {
|
|
816
|
+
const branch = flags.get("branch");
|
|
817
|
+
if (!branch)
|
|
818
|
+
throw new Error(`usage: gith ${command} -b <branch>`);
|
|
819
|
+
assertBranchName(branch);
|
|
820
|
+
return branch;
|
|
821
|
+
}
|
|
822
|
+
function localBranches() {
|
|
823
|
+
return captureArgs([
|
|
824
|
+
"for-each-ref",
|
|
825
|
+
"--format=%(refname:short)",
|
|
826
|
+
"refs/heads"
|
|
827
|
+
]).split(`
|
|
828
|
+
`).filter(Boolean);
|
|
829
|
+
}
|
|
830
|
+
function remoteBranches() {
|
|
831
|
+
return captureArgs(["ls-remote", "--heads", "origin"]).split(`
|
|
832
|
+
`).filter(Boolean).map((line) => (line.split("\t")[1] ?? "").replace(/^refs\/heads\//, ""));
|
|
833
|
+
}
|
|
834
|
+
function sidesLabel(sides) {
|
|
835
|
+
if (sides.local !== undefined && sides.origin !== undefined)
|
|
836
|
+
return "local + origin";
|
|
837
|
+
return sides.local !== undefined ? "local" : "origin";
|
|
838
|
+
}
|
|
839
|
+
function day(seconds) {
|
|
840
|
+
return new Date(seconds * 1000).toISOString().slice(0, 10);
|
|
841
|
+
}
|
|
842
|
+
function currentBranch2() {
|
|
843
|
+
return capture("rev-parse --abbrev-ref HEAD");
|
|
844
|
+
}
|
|
845
|
+
function parsePositive(flags, name) {
|
|
846
|
+
const raw = flags.get(name);
|
|
847
|
+
if (raw === undefined)
|
|
848
|
+
return;
|
|
849
|
+
if (!/^[1-9]\d*$/.test(raw))
|
|
850
|
+
throw new Error(`-${name} needs a positive whole number.`);
|
|
851
|
+
return Number(raw);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/components/maintenance/maintenance.ts
|
|
855
|
+
function switchBranch(flags) {
|
|
856
|
+
const commit = flags.get("commit");
|
|
857
|
+
if (commit !== undefined) {
|
|
858
|
+
if (flags.has("branch") || flags.size > 1)
|
|
859
|
+
throw new Error("use only one of: -b, -commit.");
|
|
860
|
+
switchToCommit(commit);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const branch = requireBranch(flags, "switch");
|
|
864
|
+
if (!hasLocal(branch) && !hasOnOrigin(branch)) {
|
|
865
|
+
createBranch(branch, isDetached() ? "HEAD" : undefined);
|
|
866
|
+
}
|
|
867
|
+
gitArgs(["switch", "--", branch]);
|
|
868
|
+
}
|
|
869
|
+
function candidates(scope, local, remote, current) {
|
|
870
|
+
const notContainer = (name) => !isContainer(name);
|
|
871
|
+
const notCurrent = (name) => name !== current;
|
|
872
|
+
if (scope === "remote") {
|
|
873
|
+
return remote.filter(notContainer).map((name) => ({ name, label: `${name} (origin)` }));
|
|
874
|
+
}
|
|
875
|
+
if (scope === "local") {
|
|
876
|
+
return local.filter((name) => notContainer(name) && notCurrent(name)).map((name) => ({ name, label: name }));
|
|
877
|
+
}
|
|
878
|
+
return local.filter((name) => notContainer(name) && notCurrent(name) && remote.includes(name)).map((name) => ({ name, label: `${name} (local + origin)` }));
|
|
879
|
+
}
|
|
880
|
+
function parseSelection(answer, count) {
|
|
881
|
+
const indexes = new Set;
|
|
882
|
+
for (const part of answer.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
883
|
+
const range = /^(\d+)-(\d+)$/.exec(part);
|
|
884
|
+
const from = Number(range ? range[1] : part);
|
|
885
|
+
const to = Number(range ? range[2] : part);
|
|
886
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || from < 1 || to > count || from > to) {
|
|
887
|
+
throw new Error(`'${part}' is not a choice from 1 to ${count}.`);
|
|
888
|
+
}
|
|
889
|
+
for (let n = from;n <= to; n++)
|
|
890
|
+
indexes.add(n - 1);
|
|
891
|
+
}
|
|
892
|
+
return [...indexes];
|
|
893
|
+
}
|
|
894
|
+
function branchTimes(output) {
|
|
895
|
+
const times = new Map;
|
|
896
|
+
for (const line of output.split(`
|
|
897
|
+
`).filter(Boolean)) {
|
|
898
|
+
const [ref = "", time = "0"] = line.split("|");
|
|
899
|
+
const seconds = Number(time);
|
|
900
|
+
if (ref.startsWith("refs/heads/")) {
|
|
901
|
+
const name = ref.slice("refs/heads/".length);
|
|
902
|
+
times.set(name, { ...times.get(name), local: seconds });
|
|
903
|
+
} else if (ref.startsWith("refs/remotes/origin/") && ref !== "refs/remotes/origin/HEAD") {
|
|
904
|
+
const name = ref.slice("refs/remotes/origin/".length);
|
|
905
|
+
times.set(name, { ...times.get(name), origin: seconds });
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return times;
|
|
909
|
+
}
|
|
910
|
+
function staleOptions(times, scope, cutoff, current) {
|
|
911
|
+
const options = [];
|
|
912
|
+
for (const [name, sides] of times) {
|
|
913
|
+
if (isContainer(name) || name === current)
|
|
914
|
+
continue;
|
|
915
|
+
if (scope === "local" && sides.local === undefined)
|
|
916
|
+
continue;
|
|
917
|
+
const last = scope === "local" ? sides.local : Math.max(sides.local ?? 0, sides.origin ?? 0);
|
|
918
|
+
if (last === undefined || last >= cutoff)
|
|
919
|
+
continue;
|
|
920
|
+
options.push({
|
|
921
|
+
name,
|
|
922
|
+
label: `${name} (${sidesLabel(sides)}, last commit ${day(last)})`
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
return options.sort((x, y) => x.name.localeCompare(y.name));
|
|
926
|
+
}
|
|
927
|
+
function detachedOptions(times, current) {
|
|
928
|
+
const options = [];
|
|
929
|
+
for (const [name, sides] of times) {
|
|
930
|
+
if (sides.local === undefined || sides.origin !== undefined)
|
|
931
|
+
continue;
|
|
932
|
+
if (isContainer(name) || name === current)
|
|
933
|
+
continue;
|
|
934
|
+
options.push({ name, label: `${name} (local only)` });
|
|
935
|
+
}
|
|
936
|
+
return options.sort((x, y) => x.name.localeCompare(y.name));
|
|
937
|
+
}
|
|
938
|
+
async function remove(flags) {
|
|
939
|
+
const name = flags.get("branch");
|
|
940
|
+
const side = onlyOne(flags, ["local", "remote"]);
|
|
941
|
+
const scope = side ?? "pair";
|
|
942
|
+
const obsoleteOn = flags.has("obsolete");
|
|
943
|
+
const detachedOn = flags.has("detached");
|
|
944
|
+
if (obsoleteOn && detachedOn)
|
|
945
|
+
throw new Error("use only one of: --obsolete, --detached.");
|
|
946
|
+
if (flags.has("days") && !obsoleteOn)
|
|
947
|
+
throw new Error("-days only applies to --obsolete.");
|
|
948
|
+
if (obsoleteOn || detachedOn) {
|
|
949
|
+
const usage = detachedOn ? "usage: gith remove --detached [-l <n>]" : "usage: gith remove --obsolete [--local] [-days <n>] [-l <n>]";
|
|
950
|
+
if (name !== undefined || obsoleteOn && side === "remote" || detachedOn && side)
|
|
951
|
+
throw new Error(usage);
|
|
952
|
+
if (detachedOn)
|
|
953
|
+
return detached(flags);
|
|
954
|
+
return obsolete(flags, side === "local" ? "local" : "pair");
|
|
955
|
+
}
|
|
956
|
+
if (name === undefined)
|
|
957
|
+
throw new Error("usage: gith remove -b <branch> [--local | --remote], or gith remove --branch to pick");
|
|
958
|
+
if (name) {
|
|
959
|
+
if (flags.has("limit"))
|
|
960
|
+
throw new Error("-limit only applies to listings: gith remove --branch.");
|
|
961
|
+
assertBranchName(name);
|
|
962
|
+
if (isContainer(name))
|
|
963
|
+
throw new Error(`'${name}' is a container and cannot be removed.`);
|
|
964
|
+
await removeBranches(scope, [name]);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
requireTerminal();
|
|
968
|
+
await removeBranches(scope, await pick(scope, parseLimit(flags)));
|
|
969
|
+
}
|
|
970
|
+
function sync(flags) {
|
|
971
|
+
noFlags2(flags, "sync");
|
|
972
|
+
gitArgs(["fetch", "--all", "--tags", "--prune"]);
|
|
973
|
+
}
|
|
974
|
+
async function reset(flags) {
|
|
975
|
+
noFlags2(flags, "reset");
|
|
976
|
+
const changes = captureArgs(["status", "--short"]).split(`
|
|
977
|
+
`).filter(Boolean);
|
|
978
|
+
const untracked = untrackedPaths();
|
|
979
|
+
if (changes.length === 0 && untracked.length === 0) {
|
|
980
|
+
console.log("Nothing to discard.");
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
console.log("Will discard:");
|
|
984
|
+
for (const line of changes)
|
|
985
|
+
console.log(` ${line}`);
|
|
986
|
+
for (const path of untracked)
|
|
987
|
+
console.log(` ?? ${path}`);
|
|
988
|
+
if (!await confirm("Discard all of this? It cannot be undone. [Y/n] ")) {
|
|
989
|
+
console.log("Nothing discarded.");
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
gitArgs(["reset", "--hard"]);
|
|
993
|
+
if (untracked.length > 0)
|
|
994
|
+
gitArgs(["clean", "-fd"]);
|
|
995
|
+
}
|
|
996
|
+
async function clean(flags) {
|
|
997
|
+
noFlags2(flags, "clean");
|
|
998
|
+
const untracked = untrackedPaths();
|
|
999
|
+
if (untracked.length === 0) {
|
|
1000
|
+
console.log("No untracked files to remove.");
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
console.log("Will remove:");
|
|
1004
|
+
for (const path of untracked)
|
|
1005
|
+
console.log(` ${path}`);
|
|
1006
|
+
if (!await confirm(`Remove these ${untracked.length} untracked paths? [Y/n] `)) {
|
|
1007
|
+
console.log("Nothing removed.");
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
gitArgs(["clean", "-fd"]);
|
|
1011
|
+
}
|
|
1012
|
+
async function choose(options, limit) {
|
|
1013
|
+
if (options.length === 0) {
|
|
1014
|
+
console.log("No branches to remove.");
|
|
1015
|
+
return [];
|
|
1016
|
+
}
|
|
1017
|
+
const shown = limit === undefined ? options : options.slice(0, limit);
|
|
1018
|
+
for (const [i, option] of shown.entries()) {
|
|
1019
|
+
console.log(` ${i + 1}. ${option.label}`);
|
|
1020
|
+
}
|
|
1021
|
+
if (shown.length < options.length) {
|
|
1022
|
+
console.log(`showing ${shown.length} of ${options.length}; -limit <n> shows more`);
|
|
1023
|
+
}
|
|
1024
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1025
|
+
try {
|
|
1026
|
+
const answer = await rl.question("Remove which? Numbers or ranges, e.g. 1,3-4 (empty cancels): ");
|
|
1027
|
+
return parseSelection(answer, shown.length).map((i) => shown[i]?.name ?? "");
|
|
1028
|
+
} finally {
|
|
1029
|
+
rl.close();
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function requireTerminal() {
|
|
1033
|
+
if (!process.stdin.isTTY) {
|
|
1034
|
+
throw new Error("pass a branch name with -b, e.g. gith remove -b <branch>; the list needs a terminal");
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function pick(scope, limit) {
|
|
1038
|
+
const remote = scope === "local" ? [] : remoteBranches();
|
|
1039
|
+
return choose(candidates(scope, localBranches(), remote, currentBranch2()), limit);
|
|
1040
|
+
}
|
|
1041
|
+
async function removeBranches(scope, names, ask2 = true) {
|
|
1042
|
+
const plan = names.map((name) => {
|
|
1043
|
+
if (scope === "remote")
|
|
1044
|
+
return { name, local: false, origin: true };
|
|
1045
|
+
if (scope === "local")
|
|
1046
|
+
return { name, local: true, origin: false };
|
|
1047
|
+
const local = hasLocal(name);
|
|
1048
|
+
const origin = hasOnOrigin(name);
|
|
1049
|
+
if (!local && !origin) {
|
|
1050
|
+
throw new Error(`'${name}' exists neither locally nor on origin.`);
|
|
1051
|
+
}
|
|
1052
|
+
return { name, local, origin };
|
|
1053
|
+
});
|
|
1054
|
+
if (plan.length > 0)
|
|
1055
|
+
console.log("Will remove:");
|
|
1056
|
+
if (ask2 && plan.length > 0 && !await confirm(plan.length === 1 ? "Remove this branch? [Y/n] " : `Remove these ${plan.length} branches? [Y/n] `)) {
|
|
1057
|
+
console.log("Nothing removed.");
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
for (const { name, local, origin } of plan) {
|
|
1061
|
+
const sides = [local && "local", origin && "origin"].filter(Boolean).join(" + ");
|
|
1062
|
+
console.log(` ${name} (${sides})`);
|
|
1063
|
+
}
|
|
1064
|
+
for (const { name, local, origin } of plan) {
|
|
1065
|
+
if (local)
|
|
1066
|
+
gitArgs(["branch", "-d", "--", name]);
|
|
1067
|
+
if (origin)
|
|
1068
|
+
gitArgs(["push", "origin", "--delete", name]);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function readTimes() {
|
|
1072
|
+
return branchTimes(captureArgs([
|
|
1073
|
+
"for-each-ref",
|
|
1074
|
+
"--format=%(refname)|%(committerdate:unix)",
|
|
1075
|
+
"refs/heads",
|
|
1076
|
+
"refs/remotes/origin"
|
|
1077
|
+
]));
|
|
1078
|
+
}
|
|
1079
|
+
async function obsolete(flags, scope) {
|
|
1080
|
+
gitArgs(["fetch", "--prune", "origin"]);
|
|
1081
|
+
const days = parsePositive(flags, "days") ?? DEFAULT_DAYS;
|
|
1082
|
+
const cutoff = Math.floor(Date.now() / 1000) - days * 86400;
|
|
1083
|
+
const options = staleOptions(readTimes(), scope, cutoff, currentBranch2());
|
|
1084
|
+
if (options.length === 0) {
|
|
1085
|
+
console.log(`No branches untouched for ${days} days.`);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const limit = parseLimit(flags);
|
|
1089
|
+
const shown = limit === undefined ? options : options.slice(0, limit);
|
|
1090
|
+
console.log(`Untouched for ${days} days or more:`);
|
|
1091
|
+
for (const [i, option] of shown.entries()) {
|
|
1092
|
+
console.log(` ${i + 1}. ${option.label}`);
|
|
1093
|
+
}
|
|
1094
|
+
if (shown.length < options.length) {
|
|
1095
|
+
console.log(`showing ${shown.length} of ${options.length}; -limit <n> shows more`);
|
|
1096
|
+
}
|
|
1097
|
+
const where = scope === "local" ? "here" : "here and on origin";
|
|
1098
|
+
const yes = await confirm(`Remove these ${shown.length} branches ${where}? [Y/n] `);
|
|
1099
|
+
if (!yes) {
|
|
1100
|
+
console.log("Nothing removed.");
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
await removeBranches(scope, shown.map((option) => option.name), false);
|
|
1104
|
+
}
|
|
1105
|
+
async function detached(flags) {
|
|
1106
|
+
requireTerminal();
|
|
1107
|
+
gitArgs(["fetch", "--prune", "origin"]);
|
|
1108
|
+
const options = detachedOptions(readTimes(), currentBranch2());
|
|
1109
|
+
await removeBranches("local", await choose(options, parseLimit(flags)));
|
|
1110
|
+
}
|
|
1111
|
+
function switchToCommit(ref) {
|
|
1112
|
+
let sha;
|
|
1113
|
+
try {
|
|
1114
|
+
sha = captureArgs(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
1115
|
+
} catch {
|
|
1116
|
+
throw new Error(`'${ref}' is not a commit in this repository.`);
|
|
1117
|
+
}
|
|
1118
|
+
assertCleanTree();
|
|
1119
|
+
gitArgs(["switch", "--detach", sha]);
|
|
1120
|
+
console.log(`HEAD is detached at ${sha.slice(0, 7)}. To keep commits made here, run: gith switch -b <name>.`);
|
|
1121
|
+
}
|
|
1122
|
+
function untrackedPaths() {
|
|
1123
|
+
return captureArgs(["clean", "-nd"]).split(`
|
|
1124
|
+
`).filter(Boolean).map((line) => line.replace(/^Would remove /, ""));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/components/open/open.ts
|
|
1128
|
+
import { spawn } from "node:child_process";
|
|
1129
|
+
function webUrl(remote) {
|
|
1130
|
+
let url = remote.trim();
|
|
1131
|
+
const scp = /^[^@/]+@([^:]+):(.+)$/.exec(url);
|
|
1132
|
+
if (scp)
|
|
1133
|
+
url = `https://${scp[1]}/${scp[2]}`;
|
|
1134
|
+
else
|
|
1135
|
+
url = url.replace(/^ssh:\/\/([^@]+@)?/, "https://");
|
|
1136
|
+
url = url.replace(/\.git$/, "").replace(/\/$/, "");
|
|
1137
|
+
if (!/^https?:\/\//.test(url))
|
|
1138
|
+
throw new Error(`origin is not a web address: ${remote}`);
|
|
1139
|
+
return url;
|
|
1140
|
+
}
|
|
1141
|
+
function open2(flags) {
|
|
1142
|
+
if (flags.size > 0)
|
|
1143
|
+
throw new Error("usage: gith open");
|
|
1144
|
+
const url = webUrl(captureArgs(["remote", "get-url", "origin"]));
|
|
1145
|
+
const [command, args] = openerFor(process.platform, url);
|
|
1146
|
+
console.log(`> ${command} ${args.join(" ")}`);
|
|
1147
|
+
if (isDryRun())
|
|
1148
|
+
return;
|
|
1149
|
+
spawn(command, args, { detached: true, stdio: "ignore" }).unref();
|
|
1150
|
+
}
|
|
1151
|
+
function openerFor(platform, url) {
|
|
1152
|
+
if (platform === "win32")
|
|
1153
|
+
return ["explorer.exe", [url]];
|
|
1154
|
+
if (platform === "darwin")
|
|
1155
|
+
return ["open", [url]];
|
|
1156
|
+
return ["xdg-open", [url]];
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// src/components/release/release.constants.ts
|
|
1160
|
+
var KINDS = ["patch", "minor", "major"];
|
|
1161
|
+
|
|
1162
|
+
// src/components/release/release.utils.ts
|
|
1163
|
+
import { execSync } from "node:child_process";
|
|
1164
|
+
function npmVersion(kind) {
|
|
1165
|
+
console.log(`> npm version ${kind}`);
|
|
1166
|
+
if (isDryRun())
|
|
1167
|
+
return;
|
|
1168
|
+
execSync(`npm version ${kind}`, { stdio: "inherit" });
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/components/release/release.ts
|
|
1172
|
+
function release(kind) {
|
|
1173
|
+
if (!kind || !KINDS.includes(kind))
|
|
1174
|
+
throw new Error("usage: gith release <patch|minor|major>");
|
|
1175
|
+
assertCleanTree();
|
|
1176
|
+
if (kind === "patch") {
|
|
1177
|
+
releasePatch();
|
|
1178
|
+
} else {
|
|
1179
|
+
releaseFromDevelop(kind);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
function releasePatch() {
|
|
1183
|
+
console.log(`
|
|
1184
|
+
\uD83D\uDCCB Starting patch release from main...
|
|
1185
|
+
`);
|
|
1186
|
+
git("checkout main");
|
|
1187
|
+
git("pull");
|
|
1188
|
+
npmVersion("patch");
|
|
1189
|
+
git("push --follow-tags");
|
|
1190
|
+
git("checkout develop");
|
|
1191
|
+
git("pull");
|
|
1192
|
+
git("rebase main");
|
|
1193
|
+
git("push --force-with-lease");
|
|
1194
|
+
git("checkout main");
|
|
1195
|
+
assertSynced(["main", "develop"]);
|
|
1196
|
+
console.log(`
|
|
1197
|
+
✅ Patch release complete!
|
|
1198
|
+
`);
|
|
1199
|
+
}
|
|
1200
|
+
function releaseFromDevelop(kind) {
|
|
1201
|
+
console.log(`
|
|
1202
|
+
\uD83D\uDCCB Starting ${kind} release from develop...
|
|
1203
|
+
`);
|
|
1204
|
+
const current = capture("rev-parse --abbrev-ref HEAD");
|
|
1205
|
+
if (current !== "develop")
|
|
1206
|
+
throw new Error(`must be on 'develop', currently on '${current}'.`);
|
|
1207
|
+
git("pull");
|
|
1208
|
+
npmVersion(kind);
|
|
1209
|
+
git("push --follow-tags");
|
|
1210
|
+
git("checkout main");
|
|
1211
|
+
git("pull");
|
|
1212
|
+
git("merge develop --no-edit");
|
|
1213
|
+
git("push");
|
|
1214
|
+
git("checkout develop");
|
|
1215
|
+
git("pull origin main");
|
|
1216
|
+
assertSynced(["main", "develop"]);
|
|
1217
|
+
console.log(`
|
|
1218
|
+
✅ ${kind} release complete!
|
|
1219
|
+
`);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/components/stash/stash.ts
|
|
1223
|
+
function stash(flags) {
|
|
1224
|
+
const name = flags.get("name");
|
|
1225
|
+
if (flags.size > (name === undefined ? 0 : 1))
|
|
1226
|
+
throw new Error("usage: gith stash [-n <name>]");
|
|
1227
|
+
if (captureArgs(["status", "--porcelain"]) === "")
|
|
1228
|
+
throw new Error("nothing to stash.");
|
|
1229
|
+
const args = ["stash", "push", "--include-untracked"];
|
|
1230
|
+
if (name)
|
|
1231
|
+
args.push("-m", name);
|
|
1232
|
+
gitArgs(args);
|
|
1233
|
+
}
|
|
1234
|
+
function unstash(flags) {
|
|
1235
|
+
if (flags.size > 0)
|
|
1236
|
+
throw new Error("usage: gith unstash");
|
|
1237
|
+
gitArgs(["stash", "pop"]);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/components/stat/stat.constants.ts
|
|
1241
|
+
var PERIODS = [
|
|
1242
|
+
"overall",
|
|
1243
|
+
"current-year",
|
|
1244
|
+
"current-month",
|
|
1245
|
+
"current-week",
|
|
1246
|
+
"last-year",
|
|
1247
|
+
"last-month",
|
|
1248
|
+
"last-week"
|
|
1249
|
+
];
|
|
1250
|
+
var DAYS = [
|
|
1251
|
+
"Sunday",
|
|
1252
|
+
"Monday",
|
|
1253
|
+
"Tuesday",
|
|
1254
|
+
"Wednesday",
|
|
1255
|
+
"Thursday",
|
|
1256
|
+
"Friday",
|
|
1257
|
+
"Saturday"
|
|
1258
|
+
];
|
|
1259
|
+
|
|
1260
|
+
// src/components/stat/stat.utils.ts
|
|
1261
|
+
function startOfWeek(date) {
|
|
1262
|
+
const offset = (date.getDay() + 6) % 7;
|
|
1263
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate() - offset);
|
|
1264
|
+
}
|
|
1265
|
+
function dayKey(date) {
|
|
1266
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1267
|
+
const day2 = String(date.getDate()).padStart(2, "0");
|
|
1268
|
+
return `${date.getFullYear()}-${month}-${day2}`;
|
|
1269
|
+
}
|
|
1270
|
+
function top(counts) {
|
|
1271
|
+
let best;
|
|
1272
|
+
for (const [key, count] of counts) {
|
|
1273
|
+
if (!best || count > best[1] || count === best[1] && key < best[0])
|
|
1274
|
+
best = [key, count];
|
|
1275
|
+
}
|
|
1276
|
+
return best;
|
|
1277
|
+
}
|
|
1278
|
+
function count(keys) {
|
|
1279
|
+
const counts = new Map;
|
|
1280
|
+
for (const key of keys)
|
|
1281
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1282
|
+
return counts;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// src/components/stat/stat.ts
|
|
1286
|
+
function periodRange(period, now) {
|
|
1287
|
+
const year = now.getFullYear();
|
|
1288
|
+
const month = now.getMonth();
|
|
1289
|
+
const week = startOfWeek(now);
|
|
1290
|
+
const iso = (date) => date.toISOString();
|
|
1291
|
+
switch (period) {
|
|
1292
|
+
case "overall":
|
|
1293
|
+
return { label: "overall" };
|
|
1294
|
+
case "current-year":
|
|
1295
|
+
return { label: `this year (${year})`, since: iso(new Date(year, 0, 1)) };
|
|
1296
|
+
case "last-year":
|
|
1297
|
+
return {
|
|
1298
|
+
label: `last year (${year - 1})`,
|
|
1299
|
+
since: iso(new Date(year - 1, 0, 1)),
|
|
1300
|
+
until: iso(new Date(year, 0, 1))
|
|
1301
|
+
};
|
|
1302
|
+
case "current-month":
|
|
1303
|
+
return { label: "this month", since: iso(new Date(year, month, 1)) };
|
|
1304
|
+
case "last-month":
|
|
1305
|
+
return {
|
|
1306
|
+
label: "last month",
|
|
1307
|
+
since: iso(new Date(year, month - 1, 1)),
|
|
1308
|
+
until: iso(new Date(year, month, 1))
|
|
1309
|
+
};
|
|
1310
|
+
case "current-week":
|
|
1311
|
+
return { label: "this week", since: iso(week) };
|
|
1312
|
+
case "last-week":
|
|
1313
|
+
return {
|
|
1314
|
+
label: "last week",
|
|
1315
|
+
since: iso(new Date(week.getFullYear(), week.getMonth(), week.getDate() - 7)),
|
|
1316
|
+
until: iso(week)
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function parseLog(output) {
|
|
1321
|
+
return output.split("\x1E").filter((record) => record.trim() !== "").map((record) => {
|
|
1322
|
+
const [header = "", ...lines] = record.trim().split(`
|
|
1323
|
+
`);
|
|
1324
|
+
const [hash = "", author = "", date = "", subject = ""] = header.split("\x1F");
|
|
1325
|
+
let added = 0;
|
|
1326
|
+
let deleted = 0;
|
|
1327
|
+
const files = [];
|
|
1328
|
+
for (const line of lines) {
|
|
1329
|
+
const [adds = "", dels = "", file = ""] = line.split("\t");
|
|
1330
|
+
if (file === "")
|
|
1331
|
+
continue;
|
|
1332
|
+
added += Number(adds) || 0;
|
|
1333
|
+
deleted += Number(dels) || 0;
|
|
1334
|
+
files.push(file);
|
|
1335
|
+
}
|
|
1336
|
+
return {
|
|
1337
|
+
hash,
|
|
1338
|
+
author,
|
|
1339
|
+
date: new Date(date),
|
|
1340
|
+
subject,
|
|
1341
|
+
added,
|
|
1342
|
+
deleted,
|
|
1343
|
+
files
|
|
1344
|
+
};
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
function longestStreak(dates) {
|
|
1348
|
+
const days = [...new Set(dates.map(dayKey))].sort();
|
|
1349
|
+
let best = 0;
|
|
1350
|
+
let run2 = 0;
|
|
1351
|
+
let previous = -Infinity;
|
|
1352
|
+
for (const key of days) {
|
|
1353
|
+
const [y = 0, m = 1, d = 1] = key.split("-").map(Number);
|
|
1354
|
+
const ms = Date.UTC(y, m - 1, d) / 86400000;
|
|
1355
|
+
run2 = ms - previous === 1 ? run2 + 1 : 1;
|
|
1356
|
+
best = Math.max(best, run2);
|
|
1357
|
+
previous = ms;
|
|
1358
|
+
}
|
|
1359
|
+
return best;
|
|
1360
|
+
}
|
|
1361
|
+
function summarize(commits2) {
|
|
1362
|
+
if (commits2.length === 0)
|
|
1363
|
+
return [];
|
|
1364
|
+
const rows = [];
|
|
1365
|
+
const add = (stat, value, detail = "") => rows.push({ stat, value, detail });
|
|
1366
|
+
const authors = count(commits2.map((c) => c.author));
|
|
1367
|
+
const topAuthor = top(authors);
|
|
1368
|
+
const oldest = commits2.reduce((a, b) => a.date <= b.date ? a : b);
|
|
1369
|
+
const newest = commits2.reduce((a, b) => a.date >= b.date ? a : b);
|
|
1370
|
+
const added = commits2.reduce((sum, c) => sum + c.added, 0);
|
|
1371
|
+
const deleted = commits2.reduce((sum, c) => sum + c.deleted, 0);
|
|
1372
|
+
add("Commits", String(commits2.length), `${dayKey(oldest.date)} to ${dayKey(newest.date)}`);
|
|
1373
|
+
add("Authors", String(authors.size));
|
|
1374
|
+
if (topAuthor)
|
|
1375
|
+
add("Top author", topAuthor[0], `${topAuthor[1]} commits`);
|
|
1376
|
+
const biggest = commits2.reduce((a, b) => b.added + b.deleted > a.added + a.deleted ? b : a);
|
|
1377
|
+
add("Biggest commit", `+${biggest.added} -${biggest.deleted}`, `${biggest.hash.slice(0, 7)} ${biggest.subject} (${biggest.author})`);
|
|
1378
|
+
add("Average size", `${Math.round((added + deleted) / commits2.length)} lines`, `per commit; +${added} -${deleted} in total`);
|
|
1379
|
+
const files = count(commits2.flatMap((c) => c.files));
|
|
1380
|
+
const topFile = top(files);
|
|
1381
|
+
add("Files touched", String(files.size), topFile ? `most changed: ${topFile[0]} (${topFile[1]} commits)` : "");
|
|
1382
|
+
const busiestDay = top(count(commits2.map((c) => dayKey(c.date))));
|
|
1383
|
+
if (busiestDay)
|
|
1384
|
+
add("Busiest day", busiestDay[0], `${busiestDay[1]} commits`);
|
|
1385
|
+
const weekday = top(count(commits2.map((c) => DAYS[c.date.getDay()] ?? "")));
|
|
1386
|
+
if (weekday)
|
|
1387
|
+
add("Busiest weekday", weekday[0], `${weekday[1]} commits`);
|
|
1388
|
+
const hour = top(count(commits2.map((c) => String(c.date.getHours()).padStart(2, "0"))));
|
|
1389
|
+
if (hour)
|
|
1390
|
+
add("Peak hour", `${hour[0]}:00`, `${hour[1]} commits`);
|
|
1391
|
+
const streak = longestStreak(commits2.map((c) => c.date));
|
|
1392
|
+
add("Longest streak", `${streak} ${streak === 1 ? "day" : "days"}`, "in a row with a commit");
|
|
1393
|
+
const nightOwl = commits2.filter((c) => c.date.getHours() >= 22 || c.date.getHours() < 5).length;
|
|
1394
|
+
const weekend = commits2.filter((c) => c.date.getDay() === 0 || c.date.getDay() === 6).length;
|
|
1395
|
+
add("Night owl", `${nightOwl} commits`, "between 22:00 and 05:00");
|
|
1396
|
+
add("Weekend", `${weekend} commits`, "on Saturday or Sunday");
|
|
1397
|
+
const types = count(commits2.map((c) => /^(\w+)(\([^)]*\))?!?:/.exec(c.subject)?.[1]?.toLowerCase() ?? "").filter(Boolean));
|
|
1398
|
+
if (types.size > 0) {
|
|
1399
|
+
const list2 = [...types.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 4).map(([type, n]) => `${type} ${n}`).join(", ");
|
|
1400
|
+
add("Commit types", list2, "from conventional prefixes");
|
|
1401
|
+
}
|
|
1402
|
+
return rows;
|
|
1403
|
+
}
|
|
1404
|
+
function table(rows) {
|
|
1405
|
+
const headers = ["Stat", "Value", "Detail"];
|
|
1406
|
+
const cells = rows.map((row) => [row.stat, row.value, row.detail]);
|
|
1407
|
+
const widths = headers.map((header, i) => Math.max(header.length, ...cells.map((cell) => cell[i]?.length ?? 0)));
|
|
1408
|
+
const line = (values) => `│ ${values.map((value, i) => value.padEnd(widths[i] ?? 0)).join(" │ ")} │`;
|
|
1409
|
+
const rule = (left, middle, right) => `${left}${widths.map((width) => "─".repeat(width + 2)).join(middle)}${right}`;
|
|
1410
|
+
return [
|
|
1411
|
+
rule("┌", "┬", "┐"),
|
|
1412
|
+
line(headers),
|
|
1413
|
+
rule("├", "┼", "┤"),
|
|
1414
|
+
...cells.map(line),
|
|
1415
|
+
rule("└", "┴", "┘")
|
|
1416
|
+
].join(`
|
|
1417
|
+
`);
|
|
1418
|
+
}
|
|
1419
|
+
function stat(flags) {
|
|
1420
|
+
const period = onlyOne(flags, PERIODS) ?? "overall";
|
|
1421
|
+
const limit = parseLimit(flags);
|
|
1422
|
+
const range = periodRange(period, new Date);
|
|
1423
|
+
const args = [
|
|
1424
|
+
"log",
|
|
1425
|
+
"--numstat",
|
|
1426
|
+
"--date=iso-strict",
|
|
1427
|
+
"--format=%x1e%H%x1f%an%x1f%aI%x1f%s"
|
|
1428
|
+
];
|
|
1429
|
+
if (range.since)
|
|
1430
|
+
args.push(`--since=${range.since}`);
|
|
1431
|
+
if (range.until)
|
|
1432
|
+
args.push(`--until=${range.until}`);
|
|
1433
|
+
if (limit !== undefined)
|
|
1434
|
+
args.push(`-n${limit}`);
|
|
1435
|
+
const commits2 = parseLog(captureArgs(args));
|
|
1436
|
+
if (commits2.length === 0) {
|
|
1437
|
+
console.log(`No commits ${range.label === "overall" ? "yet" : `in ${range.label}`}.`);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
console.log(`gith stat, ${range.label}${limit === undefined ? "" : `, last ${limit} commits`}`);
|
|
1441
|
+
console.log(table(summarize(commits2)));
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// src/components/status/status.ts
|
|
1445
|
+
function parseStatus(output) {
|
|
1446
|
+
const status = {
|
|
1447
|
+
branch: "",
|
|
1448
|
+
ahead: 0,
|
|
1449
|
+
behind: 0,
|
|
1450
|
+
staged: 0,
|
|
1451
|
+
unstaged: 0,
|
|
1452
|
+
untracked: 0,
|
|
1453
|
+
conflicts: 0
|
|
1454
|
+
};
|
|
1455
|
+
for (const line of output.split(`
|
|
1456
|
+
`)) {
|
|
1457
|
+
if (line.startsWith("# branch.head ")) {
|
|
1458
|
+
status.branch = line.slice("# branch.head ".length);
|
|
1459
|
+
} else if (line.startsWith("# branch.upstream ")) {
|
|
1460
|
+
status.upstream = line.slice("# branch.upstream ".length);
|
|
1461
|
+
} else if (line.startsWith("# branch.ab ")) {
|
|
1462
|
+
const [ahead = "+0", behind = "-0"] = line.slice("# branch.ab ".length).split(" ");
|
|
1463
|
+
status.ahead = Number(ahead);
|
|
1464
|
+
status.behind = Math.abs(Number(behind));
|
|
1465
|
+
} else if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
1466
|
+
const code = line.split(" ")[1] ?? "..";
|
|
1467
|
+
if (code[0] !== ".")
|
|
1468
|
+
status.staged++;
|
|
1469
|
+
if (code[1] !== ".")
|
|
1470
|
+
status.unstaged++;
|
|
1471
|
+
} else if (line.startsWith("u ")) {
|
|
1472
|
+
status.conflicts++;
|
|
1473
|
+
} else if (line.startsWith("? ")) {
|
|
1474
|
+
status.untracked++;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return status;
|
|
1478
|
+
}
|
|
1479
|
+
function status(flags) {
|
|
1480
|
+
if (flags.size > 0)
|
|
1481
|
+
throw new Error("usage: gith status");
|
|
1482
|
+
const info = parseStatus(captureArgs(["status", "--porcelain=v2", "--branch"]));
|
|
1483
|
+
const against = info.upstream ? `${info.branch} -> ${info.upstream} ahead ${info.ahead}, behind ${info.behind}` : `${info.branch} (no upstream)`;
|
|
1484
|
+
console.log(against);
|
|
1485
|
+
const changed = info.staged + info.unstaged + info.untracked + info.conflicts;
|
|
1486
|
+
if (changed === 0) {
|
|
1487
|
+
console.log("working tree clean");
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
console.log(`staged ${info.staged}, unstaged ${info.unstaged}, untracked ${info.untracked}, conflicts ${info.conflicts}`);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// src/components/tree/tree.utils.ts
|
|
1494
|
+
function flagsOf(worktree) {
|
|
1495
|
+
return [
|
|
1496
|
+
worktree.bare ? "bare" : "",
|
|
1497
|
+
worktree.locked ? "locked" : "",
|
|
1498
|
+
worktree.prunable ? "prunable" : ""
|
|
1499
|
+
].filter(Boolean).join(", ");
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// src/components/tree/tree.ts
|
|
1503
|
+
function parseWorktrees(output) {
|
|
1504
|
+
return output.split(/\n\s*\n/).map((block) => block.trim()).filter(Boolean).map((block) => {
|
|
1505
|
+
const worktree = {
|
|
1506
|
+
path: "",
|
|
1507
|
+
head: "",
|
|
1508
|
+
bare: false,
|
|
1509
|
+
locked: false,
|
|
1510
|
+
prunable: false
|
|
1511
|
+
};
|
|
1512
|
+
for (const line of block.split(`
|
|
1513
|
+
`)) {
|
|
1514
|
+
const [key, ...rest] = line.split(" ");
|
|
1515
|
+
const value = rest.join(" ");
|
|
1516
|
+
if (key === "worktree")
|
|
1517
|
+
worktree.path = value;
|
|
1518
|
+
else if (key === "HEAD")
|
|
1519
|
+
worktree.head = value.slice(0, 7);
|
|
1520
|
+
else if (key === "branch")
|
|
1521
|
+
worktree.branch = value.replace(/^refs\/heads\//, "");
|
|
1522
|
+
else if (key === "bare")
|
|
1523
|
+
worktree.bare = true;
|
|
1524
|
+
else if (key === "locked")
|
|
1525
|
+
worktree.locked = true;
|
|
1526
|
+
else if (key === "prunable")
|
|
1527
|
+
worktree.prunable = true;
|
|
1528
|
+
}
|
|
1529
|
+
return worktree;
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
function tree(flags) {
|
|
1533
|
+
if (flags.size > 0 && !(flags.size === 1 && flags.has("limit")))
|
|
1534
|
+
throw new Error("usage: gith tree [-limit <n>]");
|
|
1535
|
+
const limit = parseLimit(flags);
|
|
1536
|
+
const current = captureArgs(["rev-parse", "--show-toplevel"]);
|
|
1537
|
+
const worktrees = parseWorktrees(captureArgs(["worktree", "list", "--porcelain"]));
|
|
1538
|
+
const shown = limit === undefined ? worktrees : worktrees.slice(0, limit);
|
|
1539
|
+
const rows = shown.map((worktree) => ({
|
|
1540
|
+
mark: worktree.path === current ? "*" : " ",
|
|
1541
|
+
path: worktree.path,
|
|
1542
|
+
branch: worktree.bare ? "(bare)" : worktree.branch ?? "(detached)",
|
|
1543
|
+
head: worktree.bare ? "" : worktree.head,
|
|
1544
|
+
note: flagsOf(worktree)
|
|
1545
|
+
}));
|
|
1546
|
+
const width = Math.max("PATH".length, ...rows.map((row) => row.path.length));
|
|
1547
|
+
const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
|
|
1548
|
+
console.log(` ${"PATH".padEnd(width)} ${"BRANCH".padEnd(branchWidth)} HEAD`);
|
|
1549
|
+
for (const row of rows) {
|
|
1550
|
+
const line = `${row.mark} ${row.path.padEnd(width)} ${row.branch.padEnd(branchWidth)} ${row.head}`;
|
|
1551
|
+
console.log(row.note ? `${line} [${row.note}]` : line);
|
|
1552
|
+
}
|
|
1553
|
+
if (shown.length < worktrees.length) {
|
|
1554
|
+
console.log(`showing ${shown.length} of ${worktrees.length}; -limit <n> shows more`);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/components/version/version.ts
|
|
1559
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1560
|
+
|
|
1561
|
+
// src/components/version/version.constants.ts
|
|
1562
|
+
var MASCOT = [
|
|
1563
|
+
" o",
|
|
1564
|
+
" /",
|
|
1565
|
+
" o----o----o",
|
|
1566
|
+
" \\ | /",
|
|
1567
|
+
" \\ | /",
|
|
1568
|
+
" (o_o)",
|
|
1569
|
+
" /|_|\\",
|
|
1570
|
+
" d b"
|
|
1571
|
+
];
|
|
1572
|
+
|
|
1573
|
+
// src/components/version/version.utils.ts
|
|
1574
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
1575
|
+
import path from "node:path";
|
|
1576
|
+
import { fileURLToPath } from "node:url";
|
|
1577
|
+
function packageJsonPath() {
|
|
1578
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
1579
|
+
for (;; ) {
|
|
1580
|
+
const candidate = path.join(dir, "package.json");
|
|
1581
|
+
if (existsSync(candidate)) {
|
|
1582
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf8"));
|
|
1583
|
+
if (pkg.name === "@tertium/gith")
|
|
1584
|
+
return candidate;
|
|
1585
|
+
}
|
|
1586
|
+
const parent = path.dirname(dir);
|
|
1587
|
+
if (parent === dir)
|
|
1588
|
+
throw new Error("gith package.json not found");
|
|
1589
|
+
dir = parent;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/components/version/version.ts
|
|
1594
|
+
function readVersion() {
|
|
1595
|
+
const pkg = JSON.parse(readFileSync2(packageJsonPath(), "utf8"));
|
|
1596
|
+
return pkg.version ?? "unknown";
|
|
1597
|
+
}
|
|
1598
|
+
function versionText() {
|
|
1599
|
+
const version = readVersion();
|
|
1600
|
+
const side = ["", "", "", "", "", `gith v${version}`, "git happens.", ""];
|
|
1601
|
+
return MASCOT.map((line, i) => `${line.padEnd(16)}${side[i] ?? ""}`.trimEnd()).join(`
|
|
1602
|
+
`);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/cli.ts
|
|
1606
|
+
var USAGE = `gith - git helper: branch flow and everyday git commands
|
|
1607
|
+
|
|
1608
|
+
Start and finish work
|
|
1609
|
+
gith create -b <branch> work branch from develop, named by its goal (no switch)
|
|
1610
|
+
gith create -r <version> release/<version> from develop
|
|
1611
|
+
gith create -h <version> hotfix/<version> from main
|
|
1612
|
+
gith create --release | --hotfix version from the latest tag: minor for release, patch for hotfix
|
|
1613
|
+
gith switch -b <branch> switch to a branch, creating it from develop if missing (on a detached HEAD: from that commit)
|
|
1614
|
+
gith switch -commit <sha> detach HEAD at a commit, to look at it (tracked files must be clean)
|
|
1615
|
+
gith update merge origin/develop into this work branch
|
|
1616
|
+
gith merge [-r <v> | -h <v>] merge this branch into develop, release/<v> or hotfix/<v>
|
|
1617
|
+
gith merge --current-release merge this branch into the newest release (local or origin)
|
|
1618
|
+
gith merge --current-hotfix merge this branch into the newest hotfix (local or origin)
|
|
1619
|
+
gith release --patch | --minor | --major bump the version and publish
|
|
1620
|
+
|
|
1621
|
+
Fix history
|
|
1622
|
+
gith undo undo the last commit, keep its changes staged (not if pushed)
|
|
1623
|
+
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)
|
|
1624
|
+
gith squash -limit <n> [-m <msg>] combine the last n commits into one (keeps the oldest message)
|
|
1625
|
+
|
|
1626
|
+
Save and reset work
|
|
1627
|
+
gith stash [-n <name>] stash changes, including untracked files
|
|
1628
|
+
gith unstash bring back the latest stash
|
|
1629
|
+
gith reset discard uncommitted changes and untracked files
|
|
1630
|
+
gith clean remove untracked files and directories (keeps ignored)
|
|
1631
|
+
|
|
1632
|
+
Clean up branches (every removal shows what it will remove first)
|
|
1633
|
+
gith remove -b <branch> delete the branch here and on origin (either side may be missing)
|
|
1634
|
+
gith remove -b <branch> --local delete only the local branch
|
|
1635
|
+
gith remove -b <branch> --remote delete only the origin branch
|
|
1636
|
+
gith remove --branch [-l <n>] pick from branch pairs (here and on origin)
|
|
1637
|
+
gith remove --branch --local pick from local branches
|
|
1638
|
+
gith remove --branch --remote pick from origin branches
|
|
1639
|
+
gith remove --obsolete [--local] [-days <n>] remove branches untouched for 90 days, after Y/n
|
|
1640
|
+
gith remove --detached [-l <n>] pick local branches that have no origin copy
|
|
1641
|
+
gith rm alias of remove
|
|
1642
|
+
|
|
1643
|
+
Look at the repo
|
|
1644
|
+
gith status branch vs origin, and staged, unstaged and untracked counts
|
|
1645
|
+
gith list --commits [--graph] [-l <n>] newest commits on this branch (default 5)
|
|
1646
|
+
gith list --tags [-l <n>] tags with a local and origin column (newest 5 by default)
|
|
1647
|
+
gith list --stashes [-l <n>] stashes, newest first (default 5); --pockets shows them with Gorlum
|
|
1648
|
+
gith show ... same as gith list
|
|
1649
|
+
gith tree [-l <n>] working trees: path, branch, HEAD (current marked *)
|
|
1650
|
+
gith find|search [-commit <text>] [-file <pattern>] [-change <text>] [--regex] [-limit <n>]
|
|
1651
|
+
newest commits on this branch matching every filter (default 5)
|
|
1652
|
+
gith stat [period] [-limit <n>] fun facts about this branch: biggest commit, busiest day, streaks
|
|
1653
|
+
period: --overall (default), --current-year|-month|-week, --last-year|-month|-week
|
|
1654
|
+
|
|
1655
|
+
Remote and health
|
|
1656
|
+
gith sync fetch all remotes and tags, prune stale refs
|
|
1657
|
+
gith open open origin in the browser
|
|
1658
|
+
gith doctor check the repo is ready for the branch flow (exit 1 on failures)
|
|
1659
|
+
|
|
1660
|
+
Help
|
|
1661
|
+
gith help show this help
|
|
1662
|
+
gith --version show the version with the mascot
|
|
1663
|
+
--verbose (any command) also print read-only git calls
|
|
1664
|
+
--dry-run (any command) print the git commands that would run, without running them
|
|
1665
|
+
--yes (any command) answer yes to every question (for scripts)
|
|
1666
|
+
|
|
1667
|
+
Flags: a single dash always takes a value, a double dash never does.
|
|
1668
|
+
-b -r -h -l -m -n -limit -message -name -release -hotfix -branch take a value, e.g. -limit 10
|
|
1669
|
+
--patch --minor --major --local --remote --obsolete --detached --tags --commits --graph --stashes --pockets switches
|
|
1670
|
+
--branch --release --hotfix alone pick automatically (picker, latest tag)
|
|
1671
|
+
-limit caps listings and stat (or -l); -days sets the age for remove --obsolete`;
|
|
1672
|
+
var args = process.argv.slice(2).filter((arg) => {
|
|
1673
|
+
if (arg === "--verbose")
|
|
1674
|
+
setVerbose(true);
|
|
1675
|
+
else if (arg === "--dry-run")
|
|
1676
|
+
setDryRun(true);
|
|
1677
|
+
else if (arg === "--yes")
|
|
1678
|
+
setAssumeYes(true);
|
|
1679
|
+
else
|
|
1680
|
+
return true;
|
|
1681
|
+
return false;
|
|
1682
|
+
});
|
|
1683
|
+
var [command, ...rest] = args;
|
|
1684
|
+
if (isDryRun())
|
|
1685
|
+
console.log("dry run: nothing below is changed.");
|
|
1686
|
+
try {
|
|
1687
|
+
switch (command) {
|
|
1688
|
+
case "create":
|
|
1689
|
+
create(parseFlags(rest));
|
|
1690
|
+
break;
|
|
1691
|
+
case "merge":
|
|
1692
|
+
merge(parseFlags(rest));
|
|
1693
|
+
break;
|
|
1694
|
+
case "switch":
|
|
1695
|
+
switchBranch(parseFlags(rest));
|
|
1696
|
+
break;
|
|
1697
|
+
case "remove":
|
|
1698
|
+
case "rm":
|
|
1699
|
+
await remove(parseFlags(rest));
|
|
1700
|
+
break;
|
|
1701
|
+
case "sync":
|
|
1702
|
+
sync(parseFlags(rest));
|
|
1703
|
+
break;
|
|
1704
|
+
case "clean":
|
|
1705
|
+
await clean(parseFlags(rest));
|
|
1706
|
+
break;
|
|
1707
|
+
case "list":
|
|
1708
|
+
case "show":
|
|
1709
|
+
list(parseFlags(rest));
|
|
1710
|
+
break;
|
|
1711
|
+
case "status":
|
|
1712
|
+
status(parseFlags(rest));
|
|
1713
|
+
break;
|
|
1714
|
+
case "update":
|
|
1715
|
+
update(parseFlags(rest));
|
|
1716
|
+
break;
|
|
1717
|
+
case "undo":
|
|
1718
|
+
undo(parseFlags(rest));
|
|
1719
|
+
break;
|
|
1720
|
+
case "save":
|
|
1721
|
+
await save(parseFlags(rest));
|
|
1722
|
+
break;
|
|
1723
|
+
case "squash":
|
|
1724
|
+
await squash(parseFlags(rest));
|
|
1725
|
+
break;
|
|
1726
|
+
case "find":
|
|
1727
|
+
case "search":
|
|
1728
|
+
find(parseFlags(rest));
|
|
1729
|
+
break;
|
|
1730
|
+
case "stat":
|
|
1731
|
+
stat(parseFlags(rest));
|
|
1732
|
+
break;
|
|
1733
|
+
case "stash":
|
|
1734
|
+
stash(parseFlags(rest));
|
|
1735
|
+
break;
|
|
1736
|
+
case "unstash":
|
|
1737
|
+
unstash(parseFlags(rest));
|
|
1738
|
+
break;
|
|
1739
|
+
case "open":
|
|
1740
|
+
open2(parseFlags(rest));
|
|
1741
|
+
break;
|
|
1742
|
+
case "doctor":
|
|
1743
|
+
if (!doctor(parseFlags(rest)))
|
|
1744
|
+
exit(1);
|
|
1745
|
+
break;
|
|
1746
|
+
case "tree":
|
|
1747
|
+
tree(parseFlags(rest));
|
|
1748
|
+
break;
|
|
1749
|
+
case "reset":
|
|
1750
|
+
await reset(parseFlags(rest));
|
|
1751
|
+
break;
|
|
1752
|
+
case "release": {
|
|
1753
|
+
const flags = parseFlags(rest);
|
|
1754
|
+
const kind = onlyOne(flags, ["patch", "minor", "major"]);
|
|
1755
|
+
if (!kind)
|
|
1756
|
+
throw new Error("usage: gith release --patch | --minor | --major");
|
|
1757
|
+
release(kind);
|
|
1758
|
+
break;
|
|
1759
|
+
}
|
|
1760
|
+
case "--version":
|
|
1761
|
+
console.log(versionText());
|
|
1762
|
+
break;
|
|
1763
|
+
case undefined:
|
|
1764
|
+
case "help":
|
|
1765
|
+
case "--help":
|
|
1766
|
+
console.log(USAGE);
|
|
1767
|
+
break;
|
|
1768
|
+
default:
|
|
1769
|
+
console.error(`Unknown command '${command}'.
|
|
1770
|
+
|
|
1771
|
+
${USAGE}`);
|
|
1772
|
+
exit(1);
|
|
1773
|
+
}
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
console.error(`❌ ${error instanceof Error ? error.message : error}`);
|
|
1776
|
+
exit(1);
|
|
1777
|
+
}
|