@tertium/gith 0.1.0 → 0.1.1
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 +2 -0
- package/bin/gith.js +46 -32
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ gith remove --obsolete # remove branches untouched for 90 days, on
|
|
|
70
70
|
gith remove --obsolete --local # the same, here only
|
|
71
71
|
gith remove --obsolete -days 30 # untouched for 30 days instead
|
|
72
72
|
gith remove --detached # pick local branches that have no origin copy
|
|
73
|
+
gith remove --detached --all # remove all of them: lists them, then asks Y/n
|
|
73
74
|
|
|
74
75
|
# Look at the repo
|
|
75
76
|
gith status # branch vs origin, and staged, unstaged and untracked counts
|
|
@@ -155,6 +156,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
|
|
|
155
156
|
- **`stat`** reads the history of the current branch and prints a table of facts: commit count, top author, the biggest commit (lines added and removed), average size, the most changed file, the busiest day, weekday and hour, the longest streak of days with a commit, night owl and weekend commits, and the conventional commit types. The period is one switch: `--overall` (the default), `--current-year`, `--current-month`, `--current-week` (weeks start on Monday), or the matching `--last-` switch for the previous year, month or week. `-limit <n>` looks only at the last `n` commits. Stat changes nothing.
|
|
156
157
|
- **`unstash`** runs `git stash pop`. If it conflicts, git keeps the stash in the list.
|
|
157
158
|
- **`remove --obsolete`** fetches and prunes origin, then lists the branches untouched for `-days` (default 90): no commit newer than the cutoff on either side. Each line shows the sides and the last commit date, and the command asks `Remove these N branches here and on origin? [Y/n]`. Enter means yes. `--local` looks at and removes only local branches. `-limit` shows only the first `n`, and the prompt covers only those. `main`, `develop`, `release/*` and `hotfix/*` are never removed, and neither is the checked-out branch. Each branch is deleted locally first (`git branch -d`), so a branch that git does not see as merged stops the run before its origin copy is touched.
|
|
159
|
+
- **`remove --detached --all`** fetches and prunes origin, lists every local branch with no origin copy, and asks `Remove these N branches? [Y/n]` once. Nothing is deleted without the yes, and `--all` can't be combined with `-limit`.
|
|
158
160
|
- **`remove --detached`** fetches and prunes origin, then lists local branches that have no origin copy and lets you pick from them, the same as `remove --branch --local` but for branches with no origin copy only.
|
|
159
161
|
- **Every removal shows what it will remove** before deleting anything, including the direct `remove -b` form. The picker and the prompt both show it first.
|
|
160
162
|
- **`open`** opens origin in the browser, turning an `ssh` or `https` remote into a web address. GitHub and GitLab style addresses work.
|
package/bin/gith.js
CHANGED
|
@@ -30,6 +30,7 @@ var SPECS = new Map([
|
|
|
30
30
|
["current-hotfix", "switch"],
|
|
31
31
|
["pockets", "switch"],
|
|
32
32
|
["detached", "switch"],
|
|
33
|
+
["all", "switch"],
|
|
33
34
|
["overall", "switch"],
|
|
34
35
|
["current-year", "switch"],
|
|
35
36
|
["current-month", "switch"],
|
|
@@ -945,8 +946,10 @@ async function remove(flags) {
|
|
|
945
946
|
throw new Error("use only one of: --obsolete, --detached.");
|
|
946
947
|
if (flags.has("days") && !obsoleteOn)
|
|
947
948
|
throw new Error("-days only applies to --obsolete.");
|
|
949
|
+
if (flags.has("all") && !detachedOn)
|
|
950
|
+
throw new Error("--all only applies to --detached.");
|
|
948
951
|
if (obsoleteOn || detachedOn) {
|
|
949
|
-
const usage = detachedOn ? "usage: gith remove --detached [-l <n>]" : "usage: gith remove --obsolete [--local] [-days <n>] [-l <n>]";
|
|
952
|
+
const usage = detachedOn ? "usage: gith remove --detached [--all | -l <n>]" : "usage: gith remove --obsolete [--local] [-days <n>] [-l <n>]";
|
|
950
953
|
if (name !== undefined || obsoleteOn && side === "remote" || detachedOn && side)
|
|
951
954
|
throw new Error(usage);
|
|
952
955
|
if (detachedOn)
|
|
@@ -1038,7 +1041,7 @@ function pick(scope, limit) {
|
|
|
1038
1041
|
const remote = scope === "local" ? [] : remoteBranches();
|
|
1039
1042
|
return choose(candidates(scope, localBranches(), remote, currentBranch2()), limit);
|
|
1040
1043
|
}
|
|
1041
|
-
async function removeBranches(scope, names,
|
|
1044
|
+
async function removeBranches(scope, names, ask = true) {
|
|
1042
1045
|
const plan = names.map((name) => {
|
|
1043
1046
|
if (scope === "remote")
|
|
1044
1047
|
return { name, local: false, origin: true };
|
|
@@ -1053,7 +1056,7 @@ async function removeBranches(scope, names, ask2 = true) {
|
|
|
1053
1056
|
});
|
|
1054
1057
|
if (plan.length > 0)
|
|
1055
1058
|
console.log("Will remove:");
|
|
1056
|
-
if (
|
|
1059
|
+
if (ask && plan.length > 0 && !await confirm(plan.length === 1 ? "Remove this branch? [Y/n] " : `Remove these ${plan.length} branches? [Y/n] `)) {
|
|
1057
1060
|
console.log("Nothing removed.");
|
|
1058
1061
|
return;
|
|
1059
1062
|
}
|
|
@@ -1103,9 +1106,19 @@ async function obsolete(flags, scope) {
|
|
|
1103
1106
|
await removeBranches(scope, shown.map((option) => option.name), false);
|
|
1104
1107
|
}
|
|
1105
1108
|
async function detached(flags) {
|
|
1106
|
-
requireTerminal();
|
|
1107
1109
|
gitArgs(["fetch", "--prune", "origin"]);
|
|
1108
1110
|
const options = detachedOptions(readTimes(), currentBranch2());
|
|
1111
|
+
if (flags.has("all")) {
|
|
1112
|
+
if (flags.has("limit"))
|
|
1113
|
+
throw new Error("use only one of: --all, -limit.");
|
|
1114
|
+
if (options.length === 0) {
|
|
1115
|
+
console.log("No detached branches to remove.");
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
await removeBranches("local", options.map((option) => option.name));
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
requireTerminal();
|
|
1109
1122
|
await removeBranches("local", await choose(options, parseLimit(flags)));
|
|
1110
1123
|
}
|
|
1111
1124
|
function switchToCommit(ref) {
|
|
@@ -1264,8 +1277,8 @@ function startOfWeek(date) {
|
|
|
1264
1277
|
}
|
|
1265
1278
|
function dayKey(date) {
|
|
1266
1279
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1267
|
-
const
|
|
1268
|
-
return `${date.getFullYear()}-${month}-${
|
|
1280
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1281
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
1269
1282
|
}
|
|
1270
1283
|
function top(counts) {
|
|
1271
1284
|
let best;
|
|
@@ -1347,57 +1360,57 @@ function parseLog(output) {
|
|
|
1347
1360
|
function longestStreak(dates) {
|
|
1348
1361
|
const days = [...new Set(dates.map(dayKey))].sort();
|
|
1349
1362
|
let best = 0;
|
|
1350
|
-
let
|
|
1363
|
+
let run = 0;
|
|
1351
1364
|
let previous = -Infinity;
|
|
1352
1365
|
for (const key of days) {
|
|
1353
1366
|
const [y = 0, m = 1, d = 1] = key.split("-").map(Number);
|
|
1354
1367
|
const ms = Date.UTC(y, m - 1, d) / 86400000;
|
|
1355
|
-
|
|
1356
|
-
best = Math.max(best,
|
|
1368
|
+
run = ms - previous === 1 ? run + 1 : 1;
|
|
1369
|
+
best = Math.max(best, run);
|
|
1357
1370
|
previous = ms;
|
|
1358
1371
|
}
|
|
1359
1372
|
return best;
|
|
1360
1373
|
}
|
|
1361
|
-
function summarize(
|
|
1362
|
-
if (
|
|
1374
|
+
function summarize(commits) {
|
|
1375
|
+
if (commits.length === 0)
|
|
1363
1376
|
return [];
|
|
1364
1377
|
const rows = [];
|
|
1365
1378
|
const add = (stat, value, detail = "") => rows.push({ stat, value, detail });
|
|
1366
|
-
const authors = count(
|
|
1379
|
+
const authors = count(commits.map((c) => c.author));
|
|
1367
1380
|
const topAuthor = top(authors);
|
|
1368
|
-
const oldest =
|
|
1369
|
-
const newest =
|
|
1370
|
-
const added =
|
|
1371
|
-
const deleted =
|
|
1372
|
-
add("Commits", String(
|
|
1381
|
+
const oldest = commits.reduce((a, b) => a.date <= b.date ? a : b);
|
|
1382
|
+
const newest = commits.reduce((a, b) => a.date >= b.date ? a : b);
|
|
1383
|
+
const added = commits.reduce((sum, c) => sum + c.added, 0);
|
|
1384
|
+
const deleted = commits.reduce((sum, c) => sum + c.deleted, 0);
|
|
1385
|
+
add("Commits", String(commits.length), `${dayKey(oldest.date)} to ${dayKey(newest.date)}`);
|
|
1373
1386
|
add("Authors", String(authors.size));
|
|
1374
1387
|
if (topAuthor)
|
|
1375
1388
|
add("Top author", topAuthor[0], `${topAuthor[1]} commits`);
|
|
1376
|
-
const biggest =
|
|
1389
|
+
const biggest = commits.reduce((a, b) => b.added + b.deleted > a.added + a.deleted ? b : a);
|
|
1377
1390
|
add("Biggest commit", `+${biggest.added} -${biggest.deleted}`, `${biggest.hash.slice(0, 7)} ${biggest.subject} (${biggest.author})`);
|
|
1378
|
-
add("Average size", `${Math.round((added + deleted) /
|
|
1379
|
-
const files = count(
|
|
1391
|
+
add("Average size", `${Math.round((added + deleted) / commits.length)} lines`, `per commit; +${added} -${deleted} in total`);
|
|
1392
|
+
const files = count(commits.flatMap((c) => c.files));
|
|
1380
1393
|
const topFile = top(files);
|
|
1381
1394
|
add("Files touched", String(files.size), topFile ? `most changed: ${topFile[0]} (${topFile[1]} commits)` : "");
|
|
1382
|
-
const busiestDay = top(count(
|
|
1395
|
+
const busiestDay = top(count(commits.map((c) => dayKey(c.date))));
|
|
1383
1396
|
if (busiestDay)
|
|
1384
1397
|
add("Busiest day", busiestDay[0], `${busiestDay[1]} commits`);
|
|
1385
|
-
const weekday = top(count(
|
|
1398
|
+
const weekday = top(count(commits.map((c) => DAYS[c.date.getDay()] ?? "")));
|
|
1386
1399
|
if (weekday)
|
|
1387
1400
|
add("Busiest weekday", weekday[0], `${weekday[1]} commits`);
|
|
1388
|
-
const hour = top(count(
|
|
1401
|
+
const hour = top(count(commits.map((c) => String(c.date.getHours()).padStart(2, "0"))));
|
|
1389
1402
|
if (hour)
|
|
1390
1403
|
add("Peak hour", `${hour[0]}:00`, `${hour[1]} commits`);
|
|
1391
|
-
const streak = longestStreak(
|
|
1404
|
+
const streak = longestStreak(commits.map((c) => c.date));
|
|
1392
1405
|
add("Longest streak", `${streak} ${streak === 1 ? "day" : "days"}`, "in a row with a commit");
|
|
1393
|
-
const nightOwl =
|
|
1394
|
-
const weekend =
|
|
1406
|
+
const nightOwl = commits.filter((c) => c.date.getHours() >= 22 || c.date.getHours() < 5).length;
|
|
1407
|
+
const weekend = commits.filter((c) => c.date.getDay() === 0 || c.date.getDay() === 6).length;
|
|
1395
1408
|
add("Night owl", `${nightOwl} commits`, "between 22:00 and 05:00");
|
|
1396
1409
|
add("Weekend", `${weekend} commits`, "on Saturday or Sunday");
|
|
1397
|
-
const types = count(
|
|
1410
|
+
const types = count(commits.map((c) => /^(\w+)(\([^)]*\))?!?:/.exec(c.subject)?.[1]?.toLowerCase() ?? "").filter(Boolean));
|
|
1398
1411
|
if (types.size > 0) {
|
|
1399
|
-
const
|
|
1400
|
-
add("Commit types",
|
|
1412
|
+
const list = [...types.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 4).map(([type, n]) => `${type} ${n}`).join(", ");
|
|
1413
|
+
add("Commit types", list, "from conventional prefixes");
|
|
1401
1414
|
}
|
|
1402
1415
|
return rows;
|
|
1403
1416
|
}
|
|
@@ -1432,13 +1445,13 @@ function stat(flags) {
|
|
|
1432
1445
|
args.push(`--until=${range.until}`);
|
|
1433
1446
|
if (limit !== undefined)
|
|
1434
1447
|
args.push(`-n${limit}`);
|
|
1435
|
-
const
|
|
1436
|
-
if (
|
|
1448
|
+
const commits = parseLog(captureArgs(args));
|
|
1449
|
+
if (commits.length === 0) {
|
|
1437
1450
|
console.log(`No commits ${range.label === "overall" ? "yet" : `in ${range.label}`}.`);
|
|
1438
1451
|
return;
|
|
1439
1452
|
}
|
|
1440
1453
|
console.log(`gith stat, ${range.label}${limit === undefined ? "" : `, last ${limit} commits`}`);
|
|
1441
|
-
console.log(table(summarize(
|
|
1454
|
+
console.log(table(summarize(commits)));
|
|
1442
1455
|
}
|
|
1443
1456
|
|
|
1444
1457
|
// src/components/status/status.ts
|
|
@@ -1638,6 +1651,7 @@ Clean up branches (every removal shows what it will remove first)
|
|
|
1638
1651
|
gith remove --branch --remote pick from origin branches
|
|
1639
1652
|
gith remove --obsolete [--local] [-days <n>] remove branches untouched for 90 days, after Y/n
|
|
1640
1653
|
gith remove --detached [-l <n>] pick local branches that have no origin copy
|
|
1654
|
+
gith remove --detached --all remove all of them, after listing them and a Y/n question
|
|
1641
1655
|
gith rm alias of remove
|
|
1642
1656
|
|
|
1643
1657
|
Look at the repo
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tertium/gith",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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,7 +15,10 @@
|
|
|
15
15
|
"test": "bun test --no-coverage",
|
|
16
16
|
"lint": "biome check src",
|
|
17
17
|
"lint:fix": "biome check --write src",
|
|
18
|
-
"typecheck": "tsc --noEmit"
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"release:patch": "node bin/gith.js release --patch",
|
|
20
|
+
"release:minor": "node bin/gith.js release --minor",
|
|
21
|
+
"release:major": "node bin/gith.js release --major"
|
|
19
22
|
},
|
|
20
23
|
"license": "ISC",
|
|
21
24
|
"author": "Vitalii Balabanov",
|