@xuhaojun/githunk 0.2.0 → 0.3.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/README.md +23 -2
- package/dist/githunk.js +192 -34
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -5,19 +5,40 @@ A review-first Git TUI that mixes [lazygit](https://github.com/jesseduffield/laz
|
|
|
5
5
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
|
+
No Node.js required — githunk ships as a standalone binary:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
curl -fsSL https://raw.githubusercontent.com/XuHaoJun/githunk/main/install.sh | sh
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This installs the newest release into `~/.local/bin` (or `$XDG_BIN_HOME`, or `$GITHUNK_INSTALL_DIR`), verifying its checksum first. Pin a version with `sh -s -- 0.2.0`, or pass `--no-modify-path` to skip shell-startup PATH wiring. macOS and Linux only.
|
|
15
|
+
|
|
16
|
+
Alternative via npm (needs Node.js 26.1.0 or newer just to install):
|
|
17
|
+
|
|
8
18
|
```sh
|
|
9
19
|
npm install --global @xuhaojun/githunk
|
|
10
20
|
```
|
|
11
21
|
|
|
12
22
|
## Requirements
|
|
13
23
|
|
|
14
|
-
- Node.js 26.1.0 or newer
|
|
15
24
|
- Git
|
|
16
25
|
- A terminal with interactive TUI support
|
|
17
26
|
|
|
18
27
|
`gh` is optional. When it is installed and authenticated, githunk can show GitHub pull-request status; local Git review works without it.
|
|
19
28
|
|
|
20
|
-
The
|
|
29
|
+
The launcher prefers the prebuilt binary. On platforms without one it falls back to the Node.js bundle, which needs Node.js 26.1.0 or newer; the launcher enables Node's experimental FFI support automatically for OpenTUI.
|
|
30
|
+
|
|
31
|
+
## Update
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
githunk update # newest release
|
|
35
|
+
githunk update 0.3.0 # a specific version
|
|
36
|
+
githunk update --check # report without installing
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Standalone installs replace their own binary after checksum verification. npm installs update through npm instead: `npm update --global @xuhaojun/githunk`.
|
|
40
|
+
|
|
41
|
+
Installer knobs: `GITHUNK_VERSION` pins the version, `GITHUNK_INSTALL_DIR` overrides the install directory, `GITHUNK_NO_MODIFY_PATH=1` skips shell-startup PATH wiring.
|
|
21
42
|
|
|
22
43
|
## Usage
|
|
23
44
|
|
package/dist/githunk.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { chmodSync, cpSync, mkdtempSync, renameSync, rmSync } from "node:fs";
|
|
6
|
+
import { writeFile } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join as join6 } from "node:path";
|
|
9
|
+
|
|
3
10
|
// src/cli/args.ts
|
|
4
11
|
import { Command, CommanderError } from "commander";
|
|
5
12
|
// package.json
|
|
6
13
|
var package_default = {
|
|
7
14
|
name: "@xuhaojun/githunk",
|
|
8
|
-
version: "0.
|
|
15
|
+
version: "0.3.0",
|
|
9
16
|
description: "A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.",
|
|
10
17
|
type: "module",
|
|
11
18
|
bin: {
|
|
@@ -80,8 +87,9 @@ var cliVersion = typeof package_default.version === "string" && package_default.
|
|
|
80
87
|
function parseCliArgs(argv) {
|
|
81
88
|
let stdout = "";
|
|
82
89
|
let stderr = "";
|
|
90
|
+
let update;
|
|
83
91
|
const program = new Command;
|
|
84
|
-
program.name("githunk").description("A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.").version(cliVersion, "-V, --version", "output the version number").option("-p, --path <dir>", "path to the Git repository to open").argument("[path]", "path to the Git repository to open").exitOverride().configureOutput({
|
|
92
|
+
program.name("githunk").description("A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.").version(cliVersion, "-V, --version", "output the version number").option("-p, --path <dir>", "path to the Git repository to open").argument("[path]", "path to the Git repository to open").exitOverride().action(() => {}).configureOutput({
|
|
85
93
|
writeOut: (text) => {
|
|
86
94
|
stdout += text;
|
|
87
95
|
},
|
|
@@ -89,6 +97,12 @@ function parseCliArgs(argv) {
|
|
|
89
97
|
stderr += text;
|
|
90
98
|
}
|
|
91
99
|
});
|
|
100
|
+
program.command("update").description("update githunk to the newest (or a given) release").argument("[version]", "version to install; the newest release when omitted").option("--check", "report the installed and available versions without installing").action((version, options2) => {
|
|
101
|
+
update = {
|
|
102
|
+
...version === undefined ? {} : { version },
|
|
103
|
+
check: options2.check ?? false
|
|
104
|
+
};
|
|
105
|
+
});
|
|
92
106
|
try {
|
|
93
107
|
program.parse([...argv], { from: "user" });
|
|
94
108
|
} catch (error) {
|
|
@@ -102,12 +116,91 @@ function parseCliArgs(argv) {
|
|
|
102
116
|
}
|
|
103
117
|
throw error;
|
|
104
118
|
}
|
|
119
|
+
if (update !== undefined)
|
|
120
|
+
return { kind: "update", ...update };
|
|
105
121
|
const options = program.opts();
|
|
106
122
|
const positional = program.args[0];
|
|
107
123
|
const startDirectory = options.path ?? positional;
|
|
108
124
|
return startDirectory === undefined ? { kind: "start" } : { kind: "start", startDirectory };
|
|
109
125
|
}
|
|
110
126
|
|
|
127
|
+
// src/cli/update.ts
|
|
128
|
+
import { createHash } from "node:crypto";
|
|
129
|
+
import { basename, join } from "node:path";
|
|
130
|
+
function assetNameFor(platform, arch) {
|
|
131
|
+
const osToken = platform === "linux" ? "linux" : platform === "darwin" ? "darwin" : platform === "win32" ? "windows" : null;
|
|
132
|
+
const archToken = arch === "x64" ? "x64" : arch === "arm64" ? "arm64" : null;
|
|
133
|
+
if (osToken === null || archToken === null)
|
|
134
|
+
return null;
|
|
135
|
+
if (osToken === "windows" && archToken !== "x64")
|
|
136
|
+
return null;
|
|
137
|
+
return `githunk-${osToken}-${archToken}.tar.gz`;
|
|
138
|
+
}
|
|
139
|
+
function normalizeVersion(version) {
|
|
140
|
+
return version.trim().replace(/^v/, "");
|
|
141
|
+
}
|
|
142
|
+
function compareVersions(left, right) {
|
|
143
|
+
const parts = (version) => {
|
|
144
|
+
const [major = "0", minor = "0", patch = "0"] = normalizeVersion(version).split(".");
|
|
145
|
+
return [Number(major) || 0, Number(minor) || 0, Number(patch) || 0];
|
|
146
|
+
};
|
|
147
|
+
const [aMajor, aMinor, aPatch] = parts(left);
|
|
148
|
+
const [bMajor, bMinor, bPatch] = parts(right);
|
|
149
|
+
return aMajor - bMajor || aMinor - bMinor || aPatch - bPatch;
|
|
150
|
+
}
|
|
151
|
+
function isSelfManagedBinary(executablePath) {
|
|
152
|
+
const base = basename(executablePath);
|
|
153
|
+
return base === "githunk" || base === "githunk.exe";
|
|
154
|
+
}
|
|
155
|
+
function verifyChecksum(tarball, checksums, asset) {
|
|
156
|
+
const actual = createHash("sha256").update(tarball).digest("hex");
|
|
157
|
+
const line = checksums.split(`
|
|
158
|
+
`).map((entry) => entry.trim().split(/\s+/)).find((fields) => fields[fields.length - 1] === asset);
|
|
159
|
+
const expected = line?.[0];
|
|
160
|
+
if (expected === undefined || expected === "" || expected.toLowerCase() !== actual) {
|
|
161
|
+
throw new Error(`checksum mismatch for ${asset}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function applyUpdate(target, asset, env) {
|
|
165
|
+
await env.withTempDir(async (dir) => {
|
|
166
|
+
const { tarball, checksums } = await env.fetchAsset(`v${target}`, asset);
|
|
167
|
+
verifyChecksum(tarball, checksums, asset);
|
|
168
|
+
const archivePath = join(dir, asset);
|
|
169
|
+
await env.writeFile(archivePath, tarball);
|
|
170
|
+
await env.extractTarball(archivePath, dir);
|
|
171
|
+
await env.writeBinary(env.stagedBinary(dir), env.executablePath);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function runUpdate(request, env) {
|
|
175
|
+
try {
|
|
176
|
+
if (!isSelfManagedBinary(env.executablePath)) {
|
|
177
|
+
return {
|
|
178
|
+
exitCode: 1,
|
|
179
|
+
message: "githunk was installed via npm — update it with `npm update --global @xuhaojun/githunk`"
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const asset = assetNameFor(env.platform, env.arch);
|
|
183
|
+
if (asset === null) {
|
|
184
|
+
return {
|
|
185
|
+
exitCode: 1,
|
|
186
|
+
message: `no prebuilt githunk binary ships for ${env.platform}-${env.arch} — install with \`npm install -g @xuhaojun/githunk\``
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const current = normalizeVersion(env.installedVersion());
|
|
190
|
+
const target = normalizeVersion(request.version ?? normalizeVersion(await env.fetchReleaseTag()));
|
|
191
|
+
if (compareVersions(target, current) === 0) {
|
|
192
|
+
return { exitCode: 0, message: `githunk ${current} is already up to date` };
|
|
193
|
+
}
|
|
194
|
+
if (request.check) {
|
|
195
|
+
return { exitCode: 0, message: `update available: ${current} -> ${target}` };
|
|
196
|
+
}
|
|
197
|
+
await applyUpdate(target, asset, env);
|
|
198
|
+
return { exitCode: 0, message: `updated githunk ${current} -> ${target}` };
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
111
204
|
// src/main.ts
|
|
112
205
|
import { resolve as resolve4 } from "node:path";
|
|
113
206
|
import { createCliRenderer } from "@opentui/core";
|
|
@@ -991,12 +1084,12 @@ function reviewStateFor(record, currentFingerprint) {
|
|
|
991
1084
|
}
|
|
992
1085
|
|
|
993
1086
|
// src/review/working-tree-fingerprint.ts
|
|
994
|
-
import { createHash } from "node:crypto";
|
|
1087
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
995
1088
|
function utf8(value) {
|
|
996
1089
|
return new TextEncoder().encode(value);
|
|
997
1090
|
}
|
|
998
1091
|
function sha256Tuple(parts) {
|
|
999
|
-
const hash =
|
|
1092
|
+
const hash = createHash2("sha256");
|
|
1000
1093
|
for (const part of parts) {
|
|
1001
1094
|
const bytes = utf8(part);
|
|
1002
1095
|
const length = Buffer.allocUnsafe(4);
|
|
@@ -1029,7 +1122,7 @@ function fingerprintWorkingTreeFile(target, filePatch) {
|
|
|
1029
1122
|
|
|
1030
1123
|
// src/storage/local-state-file.ts
|
|
1031
1124
|
import { mkdir, open, rename, stat, unlink, lstat, link, readFile } from "node:fs/promises";
|
|
1032
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
1125
|
+
import { dirname, isAbsolute, join as join2, resolve } from "node:path";
|
|
1033
1126
|
import { randomUUID } from "node:crypto";
|
|
1034
1127
|
async function assertNoSymlinkInPath(path, pathKind) {
|
|
1035
1128
|
const absolute = resolve(path);
|
|
@@ -1059,7 +1152,7 @@ class LocalStateFile {
|
|
|
1059
1152
|
this.pathKind = options.pathKind ?? "state";
|
|
1060
1153
|
}
|
|
1061
1154
|
get path() {
|
|
1062
|
-
return this.resolvedPath ??
|
|
1155
|
+
return this.resolvedPath ?? join2(this.runner.cwd, ".git", this.relativePath);
|
|
1063
1156
|
}
|
|
1064
1157
|
async resolvePath() {
|
|
1065
1158
|
if (this.resolvedPath !== undefined)
|
|
@@ -1067,7 +1160,7 @@ class LocalStateFile {
|
|
|
1067
1160
|
const output = (await this.runner.run(["rev-parse", "--git-path", this.relativePath], { readOnly: true })).stdout.trim();
|
|
1068
1161
|
if (output.length === 0)
|
|
1069
1162
|
throw new Error(`git returned an empty path for ${this.relativePath}`);
|
|
1070
|
-
this.resolvedPath = isAbsolute(output) ? output :
|
|
1163
|
+
this.resolvedPath = isAbsolute(output) ? output : join2(this.runner.cwd, output);
|
|
1071
1164
|
return this.resolvedPath;
|
|
1072
1165
|
}
|
|
1073
1166
|
async readText() {
|
|
@@ -1976,7 +2069,7 @@ function validateUpstream(upstream) {
|
|
|
1976
2069
|
throw new Error("invalid upstream choice");
|
|
1977
2070
|
}
|
|
1978
2071
|
}
|
|
1979
|
-
async function
|
|
2072
|
+
async function fetch2(runner, remote, options = {}) {
|
|
1980
2073
|
await runner.run(remote === undefined ? ["fetch"] : ["fetch", remote], options.background === true ? { dontLog: true } : { streamOutput: true });
|
|
1981
2074
|
}
|
|
1982
2075
|
async function pull(runner, options = {}) {
|
|
@@ -2185,7 +2278,7 @@ async function listReflog(runner, options = {}) {
|
|
|
2185
2278
|
|
|
2186
2279
|
// src/git/worktrees.ts
|
|
2187
2280
|
import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
2188
|
-
import { dirname as dirname2, join as
|
|
2281
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
2189
2282
|
function finalizeEntry(entry) {
|
|
2190
2283
|
return {
|
|
2191
2284
|
path: entry.path,
|
|
@@ -2320,11 +2413,11 @@ async function readTrimmedFile(path) {
|
|
|
2320
2413
|
}
|
|
2321
2414
|
async function inProgressBranch(gitDir) {
|
|
2322
2415
|
for (const directory of ["rebase-merge", "rebase-apply"]) {
|
|
2323
|
-
const headName = await readTrimmedFile(
|
|
2416
|
+
const headName = await readTrimmedFile(join3(gitDir, directory, "head-name"));
|
|
2324
2417
|
if (headName !== undefined)
|
|
2325
2418
|
return headName.replace(/^refs\/heads\//, "");
|
|
2326
2419
|
}
|
|
2327
|
-
return await readTrimmedFile(
|
|
2420
|
+
return await readTrimmedFile(join3(gitDir, "BISECT_START"));
|
|
2328
2421
|
}
|
|
2329
2422
|
async function listWorktrees(runner) {
|
|
2330
2423
|
const repositoryPaths = await resolveRepositoryPaths(runner);
|
|
@@ -2391,7 +2484,7 @@ function worktreeRemovalRequiresForce(error) {
|
|
|
2391
2484
|
|
|
2392
2485
|
// src/git/submodules.ts
|
|
2393
2486
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
2394
|
-
import { join as
|
|
2487
|
+
import { join as join4, resolve as resolve2 } from "node:path";
|
|
2395
2488
|
|
|
2396
2489
|
// src/domain/submodule.ts
|
|
2397
2490
|
function submoduleFullName(submodule) {
|
|
@@ -2446,7 +2539,7 @@ function parseGitModules(raw) {
|
|
|
2446
2539
|
}
|
|
2447
2540
|
async function readGitModules(directory) {
|
|
2448
2541
|
try {
|
|
2449
|
-
return await readFile3(
|
|
2542
|
+
return await readFile3(join4(directory, ".gitmodules"), "utf8");
|
|
2450
2543
|
} catch (error) {
|
|
2451
2544
|
if (error instanceof Error && "code" in error) {
|
|
2452
2545
|
const code = error.code;
|
|
@@ -2457,7 +2550,7 @@ async function readGitModules(directory) {
|
|
|
2457
2550
|
}
|
|
2458
2551
|
}
|
|
2459
2552
|
async function collectSubmodules(worktreePath, parentModule, visited) {
|
|
2460
|
-
const directory = parentModule === undefined ? worktreePath :
|
|
2553
|
+
const directory = parentModule === undefined ? worktreePath : join4(worktreePath, submoduleFullPath(parentModule));
|
|
2461
2554
|
const resolved = resolve2(directory);
|
|
2462
2555
|
if (visited.has(resolved))
|
|
2463
2556
|
return [];
|
|
@@ -2941,7 +3034,7 @@ class AppController {
|
|
|
2941
3034
|
return;
|
|
2942
3035
|
if (options.background !== true)
|
|
2943
3036
|
this.logAction(LOG_ACTIONS.fetch);
|
|
2944
|
-
await this.runMutation(() => this.requireRunnerOperation((runner) =>
|
|
3037
|
+
await this.runMutation(() => this.requireRunnerOperation((runner) => fetch2(runner, remote, options)));
|
|
2945
3038
|
}
|
|
2946
3039
|
async pull(options = {}) {
|
|
2947
3040
|
if (!this.ensureWorkingTreeMutation())
|
|
@@ -5935,7 +6028,7 @@ function commitGraphRows(commits, getColor) {
|
|
|
5935
6028
|
}
|
|
5936
6029
|
|
|
5937
6030
|
// src/ui/author-style.ts
|
|
5938
|
-
import { createHash as
|
|
6031
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
5939
6032
|
var initialsCache = new Map;
|
|
5940
6033
|
var colorCache = new Map;
|
|
5941
6034
|
function randInt(bytes, max) {
|
|
@@ -5976,7 +6069,7 @@ function authorColor(authorName) {
|
|
|
5976
6069
|
const cached = colorCache.get(authorName);
|
|
5977
6070
|
if (cached !== undefined)
|
|
5978
6071
|
return cached;
|
|
5979
|
-
const hash = new Uint8Array(
|
|
6072
|
+
const hash = new Uint8Array(createHash3("md5").update(authorName).digest());
|
|
5980
6073
|
const color = hslToHex(randFloat(hash.slice(0, 4)) * 360, 0.6 + 0.4 * randFloat(hash.slice(4, 8)), 0.4 + 0.2 * randFloat(hash.slice(8, 12)));
|
|
5981
6074
|
colorCache.set(authorName, color);
|
|
5982
6075
|
return color;
|
|
@@ -15469,7 +15562,7 @@ async function loadRefsSnapshot(runner) {
|
|
|
15469
15562
|
}
|
|
15470
15563
|
|
|
15471
15564
|
// src/git/editor.ts
|
|
15472
|
-
import { join as
|
|
15565
|
+
import { join as join5, basename as basename2 } from "node:path";
|
|
15473
15566
|
function standardTerminalPreset(editor) {
|
|
15474
15567
|
return {
|
|
15475
15568
|
edit: `${editor} -- {{filename}}`,
|
|
@@ -15562,13 +15655,13 @@ function resolvePlaceholders(template, values) {
|
|
|
15562
15655
|
}
|
|
15563
15656
|
function guessEditorBase(env, gitEditor) {
|
|
15564
15657
|
if (env.GITHUNK_EDITOR !== undefined && env.GITHUNK_EDITOR.trim().length > 0)
|
|
15565
|
-
return
|
|
15658
|
+
return basename2(env.GITHUNK_EDITOR.split(" ")[0].trim());
|
|
15566
15659
|
if (gitEditor !== undefined && gitEditor.trim().length > 0)
|
|
15567
|
-
return
|
|
15660
|
+
return basename2(gitEditor.split(" ")[0].trim());
|
|
15568
15661
|
for (const key of ["GIT_EDITOR", "VISUAL", "EDITOR"]) {
|
|
15569
15662
|
const value = env[key];
|
|
15570
15663
|
if (value !== undefined && value.trim().length > 0)
|
|
15571
|
-
return
|
|
15664
|
+
return basename2(value.split(" ")[0].trim());
|
|
15572
15665
|
}
|
|
15573
15666
|
return "vi";
|
|
15574
15667
|
}
|
|
@@ -15607,7 +15700,7 @@ async function resolveEditCommand(files, options = {}) {
|
|
|
15607
15700
|
function absolutePath(repoRoot, relativePath) {
|
|
15608
15701
|
if (relativePath.startsWith("/"))
|
|
15609
15702
|
return relativePath;
|
|
15610
|
-
return
|
|
15703
|
+
return join5(repoRoot, relativePath);
|
|
15611
15704
|
}
|
|
15612
15705
|
|
|
15613
15706
|
// src/app/create-app.ts
|
|
@@ -15615,7 +15708,7 @@ import { isAbsolute as isAbsolute2, resolve as resolve3 } from "node:path";
|
|
|
15615
15708
|
|
|
15616
15709
|
// src/app/index-watcher.ts
|
|
15617
15710
|
import { watch, statSync } from "node:fs";
|
|
15618
|
-
import { basename as
|
|
15711
|
+
import { basename as basename3, dirname as dirname3 } from "node:path";
|
|
15619
15712
|
var DEFAULT_INDEX_EVENT_DEBOUNCE_MS = 50;
|
|
15620
15713
|
var BUSY_RETRY_MS = 50;
|
|
15621
15714
|
function fingerprint(path) {
|
|
@@ -15645,7 +15738,7 @@ class IndexWatcher {
|
|
|
15645
15738
|
stopped = true;
|
|
15646
15739
|
constructor(options) {
|
|
15647
15740
|
this.options = options;
|
|
15648
|
-
this.indexName =
|
|
15741
|
+
this.indexName = basename3(options.indexPath);
|
|
15649
15742
|
this.lockName = `${this.indexName}.lock`;
|
|
15650
15743
|
this.debounceMs = options.debounceMs ?? DEFAULT_INDEX_EVENT_DEBOUNCE_MS;
|
|
15651
15744
|
this.baseline = undefined;
|
|
@@ -16322,7 +16415,7 @@ function reviewHelp(focus, state) {
|
|
|
16322
16415
|
}
|
|
16323
16416
|
|
|
16324
16417
|
// src/ui/review-workspace/review-sidebar.ts
|
|
16325
|
-
import { basename as
|
|
16418
|
+
import { basename as basename4, dirname as dirname4 } from "node:path/posix";
|
|
16326
16419
|
function normalizeDiffPath(p) {
|
|
16327
16420
|
return p?.replace(/[\r\n]+$/u, "");
|
|
16328
16421
|
}
|
|
@@ -16351,10 +16444,10 @@ function sidebarFileName(file) {
|
|
|
16351
16444
|
const path = formatTerminalPath(normalizeDiffPath(file.path) ?? file.path);
|
|
16352
16445
|
const previousPath = file.previousPath ? formatTerminalPath(normalizeDiffPath(file.previousPath) ?? file.previousPath) : undefined;
|
|
16353
16446
|
if (previousPath === undefined || previousPath === path) {
|
|
16354
|
-
return
|
|
16447
|
+
return basename4(path);
|
|
16355
16448
|
}
|
|
16356
|
-
const previousName =
|
|
16357
|
-
const nextName =
|
|
16449
|
+
const previousName = basename4(previousPath);
|
|
16450
|
+
const nextName = basename4(path);
|
|
16358
16451
|
return previousName === nextName ? nextName : `${previousName} -> ${nextName}`;
|
|
16359
16452
|
}
|
|
16360
16453
|
function formatSidebarStat(prefix, value, truncated = false) {
|
|
@@ -17600,9 +17693,9 @@ function syntaxThemeForAppearance(appearance) {
|
|
|
17600
17693
|
import { parsePatchFiles as parsePatchFiles2 } from "@pierre/diffs";
|
|
17601
17694
|
|
|
17602
17695
|
// src/review/core/identity.ts
|
|
17603
|
-
import { createHash as
|
|
17696
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
17604
17697
|
function sha256Tuple2(parts) {
|
|
17605
|
-
const hash =
|
|
17698
|
+
const hash = createHash4("sha256");
|
|
17606
17699
|
for (const part of parts) {
|
|
17607
17700
|
const bytes = new TextEncoder().encode(part);
|
|
17608
17701
|
const length = Buffer.alloc(4);
|
|
@@ -21924,7 +22017,7 @@ class AppScreenController {
|
|
|
21924
22017
|
}
|
|
21925
22018
|
|
|
21926
22019
|
// src/ui/review-workspace/controller.ts
|
|
21927
|
-
import { createHash as
|
|
22020
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
21928
22021
|
|
|
21929
22022
|
// src/review/core/state.ts
|
|
21930
22023
|
function createInitialReviewState(document) {
|
|
@@ -23354,12 +23447,12 @@ function persistedFromReviewState(state) {
|
|
|
23354
23447
|
}
|
|
23355
23448
|
|
|
23356
23449
|
// src/review/storage/review-artifact-store.ts
|
|
23357
|
-
import { createHash as
|
|
23450
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
23358
23451
|
function artifactRelativePath(reviewId, artifactId) {
|
|
23359
23452
|
return `githunk/reviews/${reviewId}/${artifactId}.json`;
|
|
23360
23453
|
}
|
|
23361
23454
|
function artifactDigest(text) {
|
|
23362
|
-
return
|
|
23455
|
+
return createHash5("sha256").update(text, "utf8").digest("hex");
|
|
23363
23456
|
}
|
|
23364
23457
|
function artifactText(artifact) {
|
|
23365
23458
|
return serializeReviewArtifactV1(artifact) + `
|
|
@@ -23868,7 +23961,7 @@ class ReviewWorkspaceController {
|
|
|
23868
23961
|
artifactIdFromMarker = marker.artifactId;
|
|
23869
23962
|
const raw = await this.artifactStore.readRaw(reviewId, marker.artifactId);
|
|
23870
23963
|
if (raw !== undefined) {
|
|
23871
|
-
const digest =
|
|
23964
|
+
const digest = createHash6("sha256").update(raw, "utf8").digest("hex");
|
|
23872
23965
|
if (digest === marker.digest) {
|
|
23873
23966
|
const parsed = JSON.parse(raw);
|
|
23874
23967
|
const res = parseReviewArtifactV1(parsed);
|
|
@@ -24892,6 +24985,64 @@ ${detail}
|
|
|
24892
24985
|
if (false) {}
|
|
24893
24986
|
|
|
24894
24987
|
// src/cli.ts
|
|
24988
|
+
var RELEASES_API = "https://api.github.com/repos/XuHaoJun/githunk/releases/latest";
|
|
24989
|
+
var DOWNLOAD_BASE = "https://github.com/XuHaoJun/githunk/releases/download";
|
|
24990
|
+
async function fetchText(url) {
|
|
24991
|
+
const response = await fetch(url);
|
|
24992
|
+
if (!response.ok)
|
|
24993
|
+
throw new Error(`request failed: ${url} (${response.status})`);
|
|
24994
|
+
return response.text();
|
|
24995
|
+
}
|
|
24996
|
+
async function fetchBytes(url) {
|
|
24997
|
+
const response = await fetch(url);
|
|
24998
|
+
if (!response.ok)
|
|
24999
|
+
throw new Error(`request failed: ${url} (${response.status})`);
|
|
25000
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
25001
|
+
}
|
|
25002
|
+
function productionUpdateEnv() {
|
|
25003
|
+
return {
|
|
25004
|
+
executablePath: process.execPath,
|
|
25005
|
+
platform: process.platform,
|
|
25006
|
+
arch: process.arch,
|
|
25007
|
+
installedVersion: () => {
|
|
25008
|
+
const proc = spawnSync(process.execPath, ["--version"], { encoding: "utf8" });
|
|
25009
|
+
if (proc.status !== 0)
|
|
25010
|
+
throw new Error("could not read the installed version");
|
|
25011
|
+
return proc.stdout.trim();
|
|
25012
|
+
},
|
|
25013
|
+
fetchReleaseTag: async () => {
|
|
25014
|
+
const payload = JSON.parse(await fetchText(RELEASES_API));
|
|
25015
|
+
const tag = typeof payload === "object" && payload !== null && "tag_name" in payload ? payload.tag_name : undefined;
|
|
25016
|
+
if (typeof tag !== "string" || tag === "")
|
|
25017
|
+
throw new Error("could not read the newest release");
|
|
25018
|
+
return tag;
|
|
25019
|
+
},
|
|
25020
|
+
fetchAsset: async (tag, asset) => ({
|
|
25021
|
+
tarball: await fetchBytes(`${DOWNLOAD_BASE}/${tag}/${asset}`),
|
|
25022
|
+
checksums: await fetchText(`${DOWNLOAD_BASE}/${tag}/SHA256SUMS`)
|
|
25023
|
+
}),
|
|
25024
|
+
withTempDir: async (run) => {
|
|
25025
|
+
const dir = mkdtempSync(join6(tmpdir(), "githunk-update-"));
|
|
25026
|
+
try {
|
|
25027
|
+
return await run(dir);
|
|
25028
|
+
} finally {
|
|
25029
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25030
|
+
}
|
|
25031
|
+
},
|
|
25032
|
+
writeFile: (path, data) => writeFile(path, data),
|
|
25033
|
+
extractTarball: async (archivePath, destDir) => {
|
|
25034
|
+
const proc = spawnSync("tar", ["-xzf", archivePath, "-C", destDir]);
|
|
25035
|
+
if (proc.status !== 0)
|
|
25036
|
+
throw new Error("could not extract the release archive (need tar on PATH)");
|
|
25037
|
+
},
|
|
25038
|
+
stagedBinary: (dir) => join6(dir, `githunk-${process.platform === "win32" ? "windows" : process.platform}-${process.arch === "arm64" ? "arm64" : "x64"}`, process.platform === "win32" ? "githunk.exe" : "githunk"),
|
|
25039
|
+
writeBinary: async (stagedPath, destPath) => {
|
|
25040
|
+
cpSync(stagedPath, `${destPath}.new`);
|
|
25041
|
+
chmodSync(`${destPath}.new`, 493);
|
|
25042
|
+
renameSync(`${destPath}.new`, destPath);
|
|
25043
|
+
}
|
|
25044
|
+
};
|
|
25045
|
+
}
|
|
24895
25046
|
var result = parseCliArgs(process.argv.slice(2));
|
|
24896
25047
|
if (result.kind === "help" || result.kind === "version") {
|
|
24897
25048
|
process.stdout.write(result.text.endsWith(`
|
|
@@ -24903,6 +25054,13 @@ if (result.kind === "help" || result.kind === "version") {
|
|
|
24903
25054
|
`) ? result.message : `${result.message}
|
|
24904
25055
|
`);
|
|
24905
25056
|
process.exitCode = result.exitCode;
|
|
25057
|
+
} else if (result.kind === "update") {
|
|
25058
|
+
const outcome = await runUpdate({ ...result.version === undefined ? {} : { version: result.version }, check: result.check }, productionUpdateEnv());
|
|
25059
|
+
const stream = outcome.exitCode === 0 ? process.stdout : process.stderr;
|
|
25060
|
+
stream.write(outcome.message.endsWith(`
|
|
25061
|
+
`) ? outcome.message : `${outcome.message}
|
|
25062
|
+
`);
|
|
25063
|
+
process.exitCode = outcome.exitCode;
|
|
24906
25064
|
} else {
|
|
24907
25065
|
process.exitCode = await startApp(result.startDirectory === undefined ? {} : { startDirectory: result.startDirectory });
|
|
24908
25066
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xuhaojun/githunk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"githunk": "bin/githunk.js"
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"zod": "^4.4.3"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"@xuhaojun/githunk-darwin-arm64": "0.
|
|
42
|
-
"@xuhaojun/githunk-darwin-x64": "0.
|
|
43
|
-
"@xuhaojun/githunk-linux-arm64": "0.
|
|
44
|
-
"@xuhaojun/githunk-linux-x64": "0.
|
|
45
|
-
"@xuhaojun/githunk-windows-x64": "0.
|
|
41
|
+
"@xuhaojun/githunk-darwin-arm64": "0.3.0",
|
|
42
|
+
"@xuhaojun/githunk-darwin-x64": "0.3.0",
|
|
43
|
+
"@xuhaojun/githunk-linux-arm64": "0.3.0",
|
|
44
|
+
"@xuhaojun/githunk-linux-x64": "0.3.0",
|
|
45
|
+
"@xuhaojun/githunk-windows-x64": "0.3.0"
|
|
46
46
|
},
|
|
47
47
|
"license": "MIT",
|
|
48
48
|
"publishConfig": {
|