@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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/bin/gith.js +46 -32
  3. 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, ask2 = true) {
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 (ask2 && plan.length > 0 && !await confirm(plan.length === 1 ? "Remove this branch? [Y/n] " : `Remove these ${plan.length} branches? [Y/n] `)) {
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 day2 = String(date.getDate()).padStart(2, "0");
1268
- return `${date.getFullYear()}-${month}-${day2}`;
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 run2 = 0;
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
- run2 = ms - previous === 1 ? run2 + 1 : 1;
1356
- best = Math.max(best, run2);
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(commits2) {
1362
- if (commits2.length === 0)
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(commits2.map((c) => c.author));
1379
+ const authors = count(commits.map((c) => c.author));
1367
1380
  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)}`);
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 = commits2.reduce((a, b) => b.added + b.deleted > a.added + a.deleted ? b : a);
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) / commits2.length)} lines`, `per commit; +${added} -${deleted} in total`);
1379
- const files = count(commits2.flatMap((c) => c.files));
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(commits2.map((c) => dayKey(c.date))));
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(commits2.map((c) => DAYS[c.date.getDay()] ?? "")));
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(commits2.map((c) => String(c.date.getHours()).padStart(2, "0"))));
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(commits2.map((c) => c.date));
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 = 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;
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(commits2.map((c) => /^(\w+)(\([^)]*\))?!?:/.exec(c.subject)?.[1]?.toLowerCase() ?? "").filter(Boolean));
1410
+ const types = count(commits.map((c) => /^(\w+)(\([^)]*\))?!?:/.exec(c.subject)?.[1]?.toLowerCase() ?? "").filter(Boolean));
1398
1411
  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");
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 commits2 = parseLog(captureArgs(args));
1436
- if (commits2.length === 0) {
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(commits2)));
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.0",
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",