@n-seiji/nuthatch 0.1.5 → 0.2.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.ja.md +31 -21
- package/README.md +31 -21
- package/dist/clean-rn2476my.js +217 -0
- package/dist/cli-26d2drt6.js +30 -0
- package/dist/cli-77yanq37.js +21 -0
- package/dist/cli-9n04gsq1.js +108 -0
- package/dist/cli-a56b2ar0.js +20 -0
- package/dist/cli-b0tk5wcx.js +86 -0
- package/dist/cli-btqjpf1q.js +35 -0
- package/dist/cli-dbzx9nzg.js +40 -0
- package/dist/cli-fhta3tr8.js +126 -0
- package/dist/cli-hz0r7zw8.js +194 -0
- package/dist/cli-ph0gp56k.js +54 -0
- package/dist/cli-pick-yjd4qpd9.js +1444 -0
- package/dist/cli-s5qcjcwk.js +25 -0
- package/dist/cli-sre95k9x.js +87 -0
- package/dist/cli-update-x0r4b4xk.js +639 -0
- package/dist/cli-wcnyhk4w.js +124 -0
- package/dist/cli-y8vsqre6.js +97 -0
- package/dist/cli.js +245 -5868
- package/{shell/init.zsh → dist/init-167d1bbs.js} +22 -4
- package/dist/jump-1zkraat1.js +108 -0
- package/dist/ls-3hyjf60d.js +34 -0
- package/dist/render-ls-x4hpnsvy.js +149 -0
- package/dist/rm-eg3j8ae1.js +7 -0
- package/dist/root-49vcb8x2.js +7 -0
- package/package.json +4 -6
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
// src/commands/init.ts
|
|
2
|
+
var ZSH_TEMPLATE = `# nuthatch shell integration for zsh.
|
|
2
3
|
# Usage: eval "$(hop init zsh)"
|
|
3
|
-
#
|
|
4
|
-
# This file is a reference copy of the template embedded in
|
|
5
|
-
# src/commands/init.ts (renderInit). Keep the two in sync.
|
|
6
4
|
hop() {
|
|
5
|
+
# stdout is not a terminal (e.g. inside p=$(hop ...)): a cd here would only
|
|
6
|
+
# affect the capturing subshell and leave the caller with an empty string,
|
|
7
|
+
# so just print the path.
|
|
8
|
+
if [[ ! -t 1 ]]; then
|
|
9
|
+
command hop "$@"
|
|
10
|
+
return $?
|
|
11
|
+
fi
|
|
12
|
+
|
|
7
13
|
if [[ "$1" == "-" ]]; then
|
|
8
14
|
cd - > /dev/null || return $?
|
|
9
15
|
return 0
|
|
@@ -28,6 +34,13 @@ hop() {
|
|
|
28
34
|
--*) should_cd=0 ;;
|
|
29
35
|
esac
|
|
30
36
|
|
|
37
|
+
# Nothing to cd into: run hop with the terminal as its stdout, so a listing
|
|
38
|
+
# keeps its colors (hop only colors stdout when it is a terminal).
|
|
39
|
+
if [[ $should_cd -eq 0 ]]; then
|
|
40
|
+
command hop "$@"
|
|
41
|
+
return $?
|
|
42
|
+
fi
|
|
43
|
+
|
|
31
44
|
# "status" is a zsh read-only special parameter (mirrors $?); using it as a
|
|
32
45
|
# local name fails with "read-only variable: status". Use "rc" instead, and
|
|
33
46
|
# declare it before the command substitution so the exit code isn't
|
|
@@ -44,3 +57,8 @@ hop() {
|
|
|
44
57
|
|
|
45
58
|
return $rc
|
|
46
59
|
}
|
|
60
|
+
`;
|
|
61
|
+
var renderInit = () => ZSH_TEMPLATE;
|
|
62
|
+
export {
|
|
63
|
+
renderInit
|
|
64
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
messageOf
|
|
3
|
+
} from "./cli-ph0gp56k.js";
|
|
4
|
+
import {
|
|
5
|
+
withRepoLock
|
|
6
|
+
} from "./cli-wcnyhk4w.js";
|
|
7
|
+
import {
|
|
8
|
+
refArgumentRejection,
|
|
9
|
+
loadRepoContext,
|
|
10
|
+
resolveBranchCheckout
|
|
11
|
+
} from "./cli-y8vsqre6.js";
|
|
12
|
+
import {
|
|
13
|
+
EXIT_GENERAL_ERROR,
|
|
14
|
+
EXIT_SAFE_REJECTION,
|
|
15
|
+
ok,
|
|
16
|
+
fail
|
|
17
|
+
} from "./cli-77yanq37.js";
|
|
18
|
+
|
|
19
|
+
// src/commands/jump.ts
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
// src/domain/sanitize.ts
|
|
23
|
+
var MAX_DIR_NAME_LENGTH = 200;
|
|
24
|
+
var HASH_LENGTH = 8;
|
|
25
|
+
var HEX_RADIX = 16;
|
|
26
|
+
var FULL_HASH_HEX_LENGTH = 8;
|
|
27
|
+
var FNV_OFFSET_BASIS = 2166136261;
|
|
28
|
+
var FNV_PRIME = 16777619;
|
|
29
|
+
var shortHash = (input) => {
|
|
30
|
+
let hash = FNV_OFFSET_BASIS;
|
|
31
|
+
for (let i = 0;i < input.length; i++) {
|
|
32
|
+
hash ^= input.charCodeAt(i);
|
|
33
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
34
|
+
}
|
|
35
|
+
return (hash >>> 0).toString(HEX_RADIX).padStart(FULL_HASH_HEX_LENGTH, "0").slice(0, HASH_LENGTH);
|
|
36
|
+
};
|
|
37
|
+
var baseName = (branch) => branch.replaceAll("/", "__");
|
|
38
|
+
var truncate = (name, maxLength) => name.length > maxLength ? name.slice(0, maxLength) : name;
|
|
39
|
+
var sanitizeBranchName = (branch, isTaken) => {
|
|
40
|
+
const base = truncate(baseName(branch), MAX_DIR_NAME_LENGTH);
|
|
41
|
+
if (!isTaken(base)) {
|
|
42
|
+
return base;
|
|
43
|
+
}
|
|
44
|
+
const hash = shortHash(branch);
|
|
45
|
+
let candidate = `${truncate(base, MAX_DIR_NAME_LENGTH - hash.length - 1)}-${hash}`;
|
|
46
|
+
let suffix = 1;
|
|
47
|
+
while (isTaken(candidate)) {
|
|
48
|
+
const withSuffix = `${hash}-${suffix}`;
|
|
49
|
+
candidate = `${truncate(base, MAX_DIR_NAME_LENGTH - withSuffix.length - 1)}-${withSuffix}`;
|
|
50
|
+
suffix++;
|
|
51
|
+
}
|
|
52
|
+
return candidate;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/commands/jump.ts
|
|
56
|
+
var existingWorktreeResult = (branch, path) => ok({ path, data: { branch, created: false } });
|
|
57
|
+
var jump = async (git, fs, options) => {
|
|
58
|
+
if (options.target === "-") {
|
|
59
|
+
const previous = process.env.OLDPWD;
|
|
60
|
+
if (previous === undefined || previous.length === 0) {
|
|
61
|
+
return fail(EXIT_GENERAL_ERROR, "No previous worktree recorded (OLDPWD is unset).");
|
|
62
|
+
}
|
|
63
|
+
return ok({
|
|
64
|
+
path: previous,
|
|
65
|
+
data: { branch: options.target, created: false }
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const refRejection = refArgumentRejection(options.target, options.track);
|
|
69
|
+
if (refRejection !== null) {
|
|
70
|
+
return refRejection;
|
|
71
|
+
}
|
|
72
|
+
const context = await loadRepoContext(git, fs, options.cwd);
|
|
73
|
+
const existing = context.worktrees.find((wt) => wt.branch === options.target);
|
|
74
|
+
if (existing !== undefined) {
|
|
75
|
+
return existingWorktreeResult(options.target, existing.path);
|
|
76
|
+
}
|
|
77
|
+
if (!options.create) {
|
|
78
|
+
return fail(EXIT_SAFE_REJECTION, `No worktree for branch "${options.target}". Re-run with --create to create it.`);
|
|
79
|
+
}
|
|
80
|
+
const checkout = await resolveBranchCheckout(git, context.rootPath, options.target, options.track);
|
|
81
|
+
if (!checkout.ok) {
|
|
82
|
+
return checkout.rejection;
|
|
83
|
+
}
|
|
84
|
+
const managedDirNames = await fs.listDirNames(context.managedRoot);
|
|
85
|
+
const existingDirNames = new Set(managedDirNames.map((name) => name.toLowerCase()));
|
|
86
|
+
const dirName = sanitizeBranchName(options.target, (candidate) => existingDirNames.has(candidate.toLowerCase()));
|
|
87
|
+
const targetPath = join(context.managedRoot, dirName);
|
|
88
|
+
return withRepoLock(context.commonDir, async () => {
|
|
89
|
+
try {
|
|
90
|
+
const fresh = await loadRepoContext(git, fs, options.cwd);
|
|
91
|
+
const racedExisting = fresh.worktrees.find((wt) => wt.branch === options.target);
|
|
92
|
+
if (racedExisting !== undefined) {
|
|
93
|
+
return existingWorktreeResult(options.target, racedExisting.path);
|
|
94
|
+
}
|
|
95
|
+
await fs.mkdir(context.managedRoot);
|
|
96
|
+
await git.addWorktree(context.rootPath, targetPath, options.target, checkout.value);
|
|
97
|
+
return ok({
|
|
98
|
+
path: targetPath,
|
|
99
|
+
data: { branch: options.target, created: true }
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return fail(EXIT_SAFE_REJECTION, `Failed to create worktree: ${messageOf(error)}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
export {
|
|
107
|
+
jump
|
|
108
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
upstreamStateOf
|
|
3
|
+
} from "./cli-btqjpf1q.js";
|
|
4
|
+
import {
|
|
5
|
+
loadRepoContext,
|
|
6
|
+
worktreeDirtyState
|
|
7
|
+
} from "./cli-y8vsqre6.js";
|
|
8
|
+
import {
|
|
9
|
+
ok
|
|
10
|
+
} from "./cli-77yanq37.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/ls.ts
|
|
13
|
+
var ls = async (git, fs, options) => {
|
|
14
|
+
const context = await loadRepoContext(git, fs, options.cwd);
|
|
15
|
+
const upstreams = git.upstreamStates(context.rootPath);
|
|
16
|
+
const entries = await Promise.all(context.worktrees.map(async (wt) => {
|
|
17
|
+
const [dirtyState, states] = await Promise.all([
|
|
18
|
+
worktreeDirtyState(git, fs, context.worktrees, wt),
|
|
19
|
+
upstreams
|
|
20
|
+
]);
|
|
21
|
+
const upstream = wt.branch === null ? null : upstreamStateOf(states, wt.branch);
|
|
22
|
+
const tracking = upstream?.kind === "tracking" ? upstream : null;
|
|
23
|
+
return {
|
|
24
|
+
...wt,
|
|
25
|
+
dirty: dirtyState ?? false,
|
|
26
|
+
ahead: tracking?.ahead ?? null,
|
|
27
|
+
behind: tracking?.behind ?? null
|
|
28
|
+
};
|
|
29
|
+
}));
|
|
30
|
+
return ok({ data: entries });
|
|
31
|
+
};
|
|
32
|
+
export {
|
|
33
|
+
ls
|
|
34
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
displayWidth,
|
|
3
|
+
padToWidth,
|
|
4
|
+
truncateToWidth,
|
|
5
|
+
truncateToWidthKeepingTail,
|
|
6
|
+
SGR,
|
|
7
|
+
SGR_RESET
|
|
8
|
+
} from "./cli-fhta3tr8.js";
|
|
9
|
+
import {
|
|
10
|
+
colorEnabled
|
|
11
|
+
} from "./cli-26d2drt6.js";
|
|
12
|
+
import {
|
|
13
|
+
reportResult
|
|
14
|
+
} from "./cli-dbzx9nzg.js";
|
|
15
|
+
import {
|
|
16
|
+
shortenHome
|
|
17
|
+
} from "./cli-s5qcjcwk.js";
|
|
18
|
+
import {
|
|
19
|
+
isWithinOrSelf
|
|
20
|
+
} from "./cli-a56b2ar0.js";
|
|
21
|
+
|
|
22
|
+
// src/render-ls.ts
|
|
23
|
+
var cell = (text, tone = "plain") => ({ text, tone });
|
|
24
|
+
var paint = (value, color) => !color || value.tone === "plain" ? value.text : `${SGR[value.tone]}${value.text}${SGR_RESET}`;
|
|
25
|
+
var SHORT_SHA_LENGTH = 7;
|
|
26
|
+
var MIN_PATH_WIDTH = 12;
|
|
27
|
+
var MAX_BRANCH_WIDTH = 32;
|
|
28
|
+
var COLUMN_GAP = " ";
|
|
29
|
+
var MARKER_WIDTH = 2;
|
|
30
|
+
var KIND_ORDER = { root: 0, managed: 1, external: 2 };
|
|
31
|
+
var KIND_CELLS = {
|
|
32
|
+
root: cell("root", "magenta"),
|
|
33
|
+
managed: cell("managed", "cyan"),
|
|
34
|
+
external: cell("external", "yellow")
|
|
35
|
+
};
|
|
36
|
+
var compareEntries = (a, b) => {
|
|
37
|
+
const kindDiff = KIND_ORDER[a.kind] - KIND_ORDER[b.kind];
|
|
38
|
+
if (kindDiff !== 0) {
|
|
39
|
+
return kindDiff;
|
|
40
|
+
}
|
|
41
|
+
if (a.branch === null || b.branch === null) {
|
|
42
|
+
return (a.branch === null ? 1 : 0) - (b.branch === null ? 1 : 0);
|
|
43
|
+
}
|
|
44
|
+
return a.branch.localeCompare(b.branch);
|
|
45
|
+
};
|
|
46
|
+
var branchCell = (entry) => {
|
|
47
|
+
if (entry.branch !== null) {
|
|
48
|
+
return cell(truncateToWidth(entry.branch, MAX_BRANCH_WIDTH), "bold");
|
|
49
|
+
}
|
|
50
|
+
const sha = entry.head?.slice(0, SHORT_SHA_LENGTH) ?? "";
|
|
51
|
+
return cell(entry.bare ? "(bare)" : `(detached ${sha})`, "dim");
|
|
52
|
+
};
|
|
53
|
+
var stateCell = (entry) => {
|
|
54
|
+
if (entry.prunable) {
|
|
55
|
+
return cell("✗ missing", "red");
|
|
56
|
+
}
|
|
57
|
+
if (entry.locked) {
|
|
58
|
+
return cell("⊘ locked", "red");
|
|
59
|
+
}
|
|
60
|
+
if (entry.bare) {
|
|
61
|
+
return cell("- bare", "dim");
|
|
62
|
+
}
|
|
63
|
+
return entry.dirty ? cell("● changes", "yellow") : cell("✓ clean", "green");
|
|
64
|
+
};
|
|
65
|
+
var syncCells = (entry) => {
|
|
66
|
+
if (entry.ahead === null || entry.behind === null) {
|
|
67
|
+
return [cell("-", "dim")];
|
|
68
|
+
}
|
|
69
|
+
if (entry.ahead === 0 && entry.behind === 0) {
|
|
70
|
+
return [cell("=", "dim")];
|
|
71
|
+
}
|
|
72
|
+
const cells = [];
|
|
73
|
+
if (entry.ahead > 0) {
|
|
74
|
+
cells.push(cell(`↑${entry.ahead}`, "green"));
|
|
75
|
+
}
|
|
76
|
+
if (entry.behind > 0) {
|
|
77
|
+
cells.push(cell(`↓${entry.behind}`, "red"));
|
|
78
|
+
}
|
|
79
|
+
return cells;
|
|
80
|
+
};
|
|
81
|
+
var currentPath = (entries, cwd) => entries.filter((entry) => isWithinOrSelf(entry.path, cwd)).reduce((best, entry) => best === null || entry.path.length > best.length ? entry.path : best, null);
|
|
82
|
+
var summaryLine = (entries) => {
|
|
83
|
+
const parts = [`${entries.length} worktree${entries.length === 1 ? "" : "s"}`];
|
|
84
|
+
const counts = [
|
|
85
|
+
[entries.filter((entry) => entry.dirty).length, "with changes"],
|
|
86
|
+
[entries.filter((entry) => (entry.ahead ?? 0) > 0).length, "ahead"],
|
|
87
|
+
[entries.filter((entry) => entry.kind === "external").length, "external"],
|
|
88
|
+
[entries.filter((entry) => entry.locked || entry.prunable).length, "locked or missing"]
|
|
89
|
+
];
|
|
90
|
+
for (const [count, label] of counts) {
|
|
91
|
+
if (count > 0) {
|
|
92
|
+
parts.push(`${count} ${label}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return parts.join(" · ");
|
|
96
|
+
};
|
|
97
|
+
var HEADERS = ["BRANCH", "STATE", "REMOTE", "KIND"];
|
|
98
|
+
var buildRow = (entry, current, home) => ({
|
|
99
|
+
marker: entry.path === current ? cell("@", "green") : cell(" "),
|
|
100
|
+
columns: [[branchCell(entry)], [stateCell(entry)], syncCells(entry), [KIND_CELLS[entry.kind]]],
|
|
101
|
+
path: shortenHome(entry.path, home)
|
|
102
|
+
});
|
|
103
|
+
var cellsWidth = (cells) => displayWidth(cells.map((part) => part.text).join(" "));
|
|
104
|
+
var renderCells = (cells, width, color) => {
|
|
105
|
+
const text = cells.map((part) => paint(part, color)).join(" ");
|
|
106
|
+
return text + " ".repeat(Math.max(0, width - cellsWidth(cells)));
|
|
107
|
+
};
|
|
108
|
+
var formatLsTable = (entries, options) => {
|
|
109
|
+
const sorted = entries.toSorted(compareEntries);
|
|
110
|
+
const current = currentPath(sorted, options.cwd);
|
|
111
|
+
const rows = sorted.map((entry) => buildRow(entry, current, options.home));
|
|
112
|
+
const widths = HEADERS.map((header, index) => Math.max(displayWidth(header), ...rows.map((row) => cellsWidth(row.columns[index] ?? []))));
|
|
113
|
+
const fixedWidth = MARKER_WIDTH + widths.reduce((total, width) => total + width + COLUMN_GAP.length, 0);
|
|
114
|
+
const pathWidth = options.width === null ? null : Math.max(MIN_PATH_WIDTH, options.width - fixedWidth);
|
|
115
|
+
const fitPath = (path) => pathWidth === null ? path : truncateToWidthKeepingTail(path, pathWidth);
|
|
116
|
+
const header = ` ${HEADERS.map((title, index) => padToWidth(title, widths[index] ?? 0)).join(COLUMN_GAP)}${COLUMN_GAP}PATH`;
|
|
117
|
+
const lines = [paint(cell(header, "dim"), options.color)];
|
|
118
|
+
for (const row of rows) {
|
|
119
|
+
const columns = row.columns.map((cells, index) => renderCells(cells, widths[index] ?? 0, options.color));
|
|
120
|
+
const path = paint(cell(fitPath(row.path), "dim"), options.color);
|
|
121
|
+
lines.push(`${paint(row.marker, options.color)} ${columns.join(COLUMN_GAP)}${COLUMN_GAP}${path}`);
|
|
122
|
+
}
|
|
123
|
+
const summary = cell(summaryLine(sorted), "dim");
|
|
124
|
+
lines.push("", paint(summary, options.color));
|
|
125
|
+
return `${lines.join(`
|
|
126
|
+
`)}
|
|
127
|
+
`;
|
|
128
|
+
};
|
|
129
|
+
var reportLs = (result, json) => {
|
|
130
|
+
if (json || !result.ok || result.data === undefined) {
|
|
131
|
+
reportResult("ls", result, json);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const color = colorEnabled(process.stdout);
|
|
135
|
+
process.stdout.write(formatLsTable(result.data, {
|
|
136
|
+
color,
|
|
137
|
+
width: process.stdout.isTTY === true ? process.stdout.columns ?? null : null,
|
|
138
|
+
home: process.env["HOME"] ?? "",
|
|
139
|
+
cwd: process.cwd()
|
|
140
|
+
}));
|
|
141
|
+
for (const warning of result.warnings ?? []) {
|
|
142
|
+
process.stderr.write(`warning: ${warning}
|
|
143
|
+
`);
|
|
144
|
+
}
|
|
145
|
+
process.exitCode = result.exitCode;
|
|
146
|
+
};
|
|
147
|
+
export {
|
|
148
|
+
reportLs
|
|
149
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@n-seiji/nuthatch",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "git worktree manager (hop)",
|
|
5
5
|
"homepage": "https://github.com/n-seiji/nuthatch#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -15,15 +15,14 @@
|
|
|
15
15
|
"hop": "dist/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"dist"
|
|
19
|
-
"shell"
|
|
18
|
+
"dist"
|
|
20
19
|
],
|
|
21
20
|
"type": "module",
|
|
22
21
|
"publishConfig": {
|
|
23
22
|
"access": "public"
|
|
24
23
|
},
|
|
25
24
|
"scripts": {
|
|
26
|
-
"build": "bun build ./src/cli.ts --
|
|
25
|
+
"build": "bun build ./src/cli.ts --outdir dist --target node --format esm --splitting",
|
|
27
26
|
"typecheck": "tsc --noEmit",
|
|
28
27
|
"lint": "oxlint .",
|
|
29
28
|
"lint:fix": "oxlint . --fix",
|
|
@@ -32,12 +31,11 @@
|
|
|
32
31
|
"test": "bun test"
|
|
33
32
|
},
|
|
34
33
|
"dependencies": {
|
|
35
|
-
"citty": "^0.1.6",
|
|
36
34
|
"valibot": "^1.4.2"
|
|
37
35
|
},
|
|
38
36
|
"devDependencies": {
|
|
39
37
|
"@types/bun": "latest",
|
|
40
|
-
"oxfmt": "^0.
|
|
38
|
+
"oxfmt": "^0.72.0",
|
|
41
39
|
"oxlint": "^1.81.0",
|
|
42
40
|
"typescript": "^5.6.0"
|
|
43
41
|
},
|