@xuhaojun/githunk 0.1.1 → 0.1.2
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 +9 -0
- package/dist/githunk.js +191 -34
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -28,6 +28,15 @@ cd path/to/repository
|
|
|
28
28
|
githunk
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
Or point it at a repository from anywhere:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
githunk --path path/to/repository
|
|
35
|
+
githunk path/to/repository
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`githunk --help` prints all options; `githunk --version` prints the installed version.
|
|
39
|
+
|
|
31
40
|
## Development
|
|
32
41
|
|
|
33
42
|
This repository uses Bun for development and for producing the Node.js bundle published to npm:
|
package/dist/githunk.js
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/cli/args.ts
|
|
4
|
+
import { Command, CommanderError } from "commander";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
var cachedVersion;
|
|
9
|
+
function getCliVersion() {
|
|
10
|
+
if (cachedVersion !== undefined)
|
|
11
|
+
return cachedVersion;
|
|
12
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
for (let level = 0;level < 4; level += 1) {
|
|
14
|
+
try {
|
|
15
|
+
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
16
|
+
if (manifest.name === "@xuhaojun/githunk" && typeof manifest.version === "string") {
|
|
17
|
+
cachedVersion = manifest.version;
|
|
18
|
+
return cachedVersion;
|
|
19
|
+
}
|
|
20
|
+
} catch {}
|
|
21
|
+
const parent = dirname(directory);
|
|
22
|
+
if (parent === directory)
|
|
23
|
+
break;
|
|
24
|
+
directory = parent;
|
|
25
|
+
}
|
|
26
|
+
cachedVersion = "0.0.0-dev";
|
|
27
|
+
return cachedVersion;
|
|
28
|
+
}
|
|
29
|
+
function parseCliArgs(argv) {
|
|
30
|
+
let stdout = "";
|
|
31
|
+
let stderr = "";
|
|
32
|
+
const program = new Command;
|
|
33
|
+
program.name("githunk").description("A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.").version(getCliVersion(), "-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({
|
|
34
|
+
writeOut: (text) => {
|
|
35
|
+
stdout += text;
|
|
36
|
+
},
|
|
37
|
+
writeErr: (text) => {
|
|
38
|
+
stderr += text;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
program.parse([...argv], { from: "user" });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof CommanderError) {
|
|
45
|
+
if (error.code === "commander.helpDisplayed")
|
|
46
|
+
return { kind: "help", text: stdout };
|
|
47
|
+
if (error.code === "commander.version")
|
|
48
|
+
return { kind: "version", text: stdout };
|
|
49
|
+
const message = stderr.trim() === "" ? error.message : stderr.trim();
|
|
50
|
+
return { kind: "error", message, exitCode: error.exitCode };
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
const options = program.opts();
|
|
55
|
+
const positional = program.args[0];
|
|
56
|
+
const startDirectory = options.path ?? positional;
|
|
57
|
+
return startDirectory === undefined ? { kind: "start" } : { kind: "start", startDirectory };
|
|
58
|
+
}
|
|
59
|
+
|
|
3
60
|
// src/main.ts
|
|
61
|
+
import { resolve as resolve4 } from "node:path";
|
|
4
62
|
import { createCliRenderer } from "@opentui/core";
|
|
5
63
|
|
|
6
64
|
// src/domain/command.ts
|
|
@@ -775,6 +833,7 @@ function withCommitStatuses(commits, sets) {
|
|
|
775
833
|
|
|
776
834
|
// src/git/commits.ts
|
|
777
835
|
var LOG_FORMAT = "%H%n%h%n%P%n%an%n%aI%n%s%n%b";
|
|
836
|
+
var COMMITS_LIMIT = 300;
|
|
778
837
|
function parseSummary(record) {
|
|
779
838
|
const fields = record.split(`
|
|
780
839
|
`);
|
|
@@ -802,8 +861,11 @@ function parseSummary(record) {
|
|
|
802
861
|
function parseCommitLog(raw) {
|
|
803
862
|
return raw.split("\x00").map(parseSummary).filter((summary) => summary !== undefined);
|
|
804
863
|
}
|
|
805
|
-
async function listCommits(runner, range, filter) {
|
|
806
|
-
const args = ["log", "-z", "--topo-order", `--format=${LOG_FORMAT}
|
|
864
|
+
async function listCommits(runner, range, filter, options) {
|
|
865
|
+
const args = ["log", "-z", "--topo-order", `--format=${LOG_FORMAT}`];
|
|
866
|
+
if (options?.limit ?? true)
|
|
867
|
+
args.push(`-${COMMITS_LIMIT}`);
|
|
868
|
+
args.push(range);
|
|
807
869
|
if (filter !== undefined && filter.length > 0)
|
|
808
870
|
args.push("--", filter);
|
|
809
871
|
const [result, statusSets] = await Promise.all([
|
|
@@ -916,7 +978,7 @@ function fingerprintWorkingTreeFile(target, filePatch) {
|
|
|
916
978
|
|
|
917
979
|
// src/storage/local-state-file.ts
|
|
918
980
|
import { mkdir, open, rename, stat, unlink, lstat, link, readFile } from "node:fs/promises";
|
|
919
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
981
|
+
import { dirname as dirname2, isAbsolute, join as join2, resolve } from "node:path";
|
|
920
982
|
import { randomUUID } from "node:crypto";
|
|
921
983
|
async function assertNoSymlinkInPath(path, pathKind) {
|
|
922
984
|
const absolute = resolve(path);
|
|
@@ -946,7 +1008,7 @@ class LocalStateFile {
|
|
|
946
1008
|
this.pathKind = options.pathKind ?? "state";
|
|
947
1009
|
}
|
|
948
1010
|
get path() {
|
|
949
|
-
return this.resolvedPath ??
|
|
1011
|
+
return this.resolvedPath ?? join2(this.runner.cwd, ".git", this.relativePath);
|
|
950
1012
|
}
|
|
951
1013
|
async resolvePath() {
|
|
952
1014
|
if (this.resolvedPath !== undefined)
|
|
@@ -954,7 +1016,7 @@ class LocalStateFile {
|
|
|
954
1016
|
const output = (await this.runner.run(["rev-parse", "--git-path", this.relativePath], { readOnly: true })).stdout.trim();
|
|
955
1017
|
if (output.length === 0)
|
|
956
1018
|
throw new Error(`git returned an empty path for ${this.relativePath}`);
|
|
957
|
-
this.resolvedPath = isAbsolute(output) ? output :
|
|
1019
|
+
this.resolvedPath = isAbsolute(output) ? output : join2(this.runner.cwd, output);
|
|
958
1020
|
return this.resolvedPath;
|
|
959
1021
|
}
|
|
960
1022
|
async readText() {
|
|
@@ -971,7 +1033,7 @@ class LocalStateFile {
|
|
|
971
1033
|
async writeText(text) {
|
|
972
1034
|
const path = await this.resolvePath();
|
|
973
1035
|
await assertNoSymlinkInPath(path, this.pathKind);
|
|
974
|
-
await mkdir(
|
|
1036
|
+
await mkdir(dirname2(path), { recursive: true, mode: 448 });
|
|
975
1037
|
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
976
1038
|
await assertNoSymlinkInPath(temporary, this.pathKind);
|
|
977
1039
|
const handle = await open(temporary, "wx", 384);
|
|
@@ -984,7 +1046,7 @@ class LocalStateFile {
|
|
|
984
1046
|
}
|
|
985
1047
|
await rename(temporary, path);
|
|
986
1048
|
try {
|
|
987
|
-
const directory = await open(
|
|
1049
|
+
const directory = await open(dirname2(path), "r");
|
|
988
1050
|
try {
|
|
989
1051
|
await directory.sync();
|
|
990
1052
|
} finally {
|
|
@@ -1008,7 +1070,7 @@ class LocalStateFile {
|
|
|
1008
1070
|
async createTextExclusive(text) {
|
|
1009
1071
|
const path = await this.resolvePath();
|
|
1010
1072
|
await assertNoSymlinkInPath(path, this.pathKind);
|
|
1011
|
-
await mkdir(
|
|
1073
|
+
await mkdir(dirname2(path), { recursive: true, mode: 448 });
|
|
1012
1074
|
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
1013
1075
|
await assertNoSymlinkInPath(temporary, this.pathKind);
|
|
1014
1076
|
const handle = await open(temporary, "wx", 384);
|
|
@@ -1037,7 +1099,7 @@ class LocalStateFile {
|
|
|
1037
1099
|
return;
|
|
1038
1100
|
});
|
|
1039
1101
|
try {
|
|
1040
|
-
const directory = await open(
|
|
1102
|
+
const directory = await open(dirname2(path), "r");
|
|
1041
1103
|
try {
|
|
1042
1104
|
await directory.sync();
|
|
1043
1105
|
} finally {
|
|
@@ -2072,7 +2134,7 @@ async function listReflog(runner, options = {}) {
|
|
|
2072
2134
|
|
|
2073
2135
|
// src/git/worktrees.ts
|
|
2074
2136
|
import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
2075
|
-
import { dirname as
|
|
2137
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
2076
2138
|
function finalizeEntry(entry) {
|
|
2077
2139
|
return {
|
|
2078
2140
|
path: entry.path,
|
|
@@ -2174,7 +2236,7 @@ async function resolveRepositoryPaths(runner) {
|
|
|
2174
2236
|
const worktreeGitDirPath = lines[1] ?? "";
|
|
2175
2237
|
const repoGitDirPath = lines[2] ?? "";
|
|
2176
2238
|
const isSubmodule = (lines[3] ?? "").length > 0;
|
|
2177
|
-
const repoPath = worktreeGitDirPath === repoGitDirPath || isSubmodule ? worktreePath :
|
|
2239
|
+
const repoPath = worktreeGitDirPath === repoGitDirPath || isSubmodule ? worktreePath : dirname3(repoGitDirPath);
|
|
2178
2240
|
return { worktreePath, worktreeGitDirPath, repoPath, repoGitDirPath };
|
|
2179
2241
|
}
|
|
2180
2242
|
async function isPathMissing(path) {
|
|
@@ -2207,11 +2269,11 @@ async function readTrimmedFile(path) {
|
|
|
2207
2269
|
}
|
|
2208
2270
|
async function inProgressBranch(gitDir) {
|
|
2209
2271
|
for (const directory of ["rebase-merge", "rebase-apply"]) {
|
|
2210
|
-
const headName = await readTrimmedFile(
|
|
2272
|
+
const headName = await readTrimmedFile(join3(gitDir, directory, "head-name"));
|
|
2211
2273
|
if (headName !== undefined)
|
|
2212
2274
|
return headName.replace(/^refs\/heads\//, "");
|
|
2213
2275
|
}
|
|
2214
|
-
return await readTrimmedFile(
|
|
2276
|
+
return await readTrimmedFile(join3(gitDir, "BISECT_START"));
|
|
2215
2277
|
}
|
|
2216
2278
|
async function listWorktrees(runner) {
|
|
2217
2279
|
const repositoryPaths = await resolveRepositoryPaths(runner);
|
|
@@ -2278,7 +2340,7 @@ function worktreeRemovalRequiresForce(error) {
|
|
|
2278
2340
|
|
|
2279
2341
|
// src/git/submodules.ts
|
|
2280
2342
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
2281
|
-
import { join as
|
|
2343
|
+
import { join as join4, resolve as resolve2 } from "node:path";
|
|
2282
2344
|
|
|
2283
2345
|
// src/domain/submodule.ts
|
|
2284
2346
|
function submoduleFullName(submodule) {
|
|
@@ -2333,7 +2395,7 @@ function parseGitModules(raw) {
|
|
|
2333
2395
|
}
|
|
2334
2396
|
async function readGitModules(directory) {
|
|
2335
2397
|
try {
|
|
2336
|
-
return await readFile3(
|
|
2398
|
+
return await readFile3(join4(directory, ".gitmodules"), "utf8");
|
|
2337
2399
|
} catch (error) {
|
|
2338
2400
|
if (error instanceof Error && "code" in error) {
|
|
2339
2401
|
const code = error.code;
|
|
@@ -2344,7 +2406,7 @@ async function readGitModules(directory) {
|
|
|
2344
2406
|
}
|
|
2345
2407
|
}
|
|
2346
2408
|
async function collectSubmodules(worktreePath, parentModule, visited) {
|
|
2347
|
-
const directory = parentModule === undefined ? worktreePath :
|
|
2409
|
+
const directory = parentModule === undefined ? worktreePath : join4(worktreePath, submoduleFullPath(parentModule));
|
|
2348
2410
|
const resolved = resolve2(directory);
|
|
2349
2411
|
if (visited.has(resolved))
|
|
2350
2412
|
return [];
|
|
@@ -2474,6 +2536,7 @@ class AppController {
|
|
|
2474
2536
|
loadCommitDetails;
|
|
2475
2537
|
loadCommitFile;
|
|
2476
2538
|
generation = 0;
|
|
2539
|
+
limitCommits = true;
|
|
2477
2540
|
pullRequestRefreshGeneration = 0;
|
|
2478
2541
|
currentState;
|
|
2479
2542
|
reviewDatabase = emptyWorkingTreeReviewDatabase();
|
|
@@ -2494,7 +2557,7 @@ class AppController {
|
|
|
2494
2557
|
return loadWorkingTree(runner, target2.scope, snapshotOptions ?? {});
|
|
2495
2558
|
});
|
|
2496
2559
|
this.loadBranchesListing = options instanceof GitRunner ? () => listBranches(options) : options.loadBranches ?? options.branchesLoader ?? (runner !== undefined ? () => listBranches(runner) : async () => ({ detached: true, localBranches: [], remotes: [] }));
|
|
2497
|
-
this.loadCommitList = options instanceof GitRunner ? (range, filter) => listCommits(options, range, filter) : options.loadCommits ?? options.commitsLoader ?? (runner === undefined ? async () => [] : (range, filter) => listCommits(runner, range, filter));
|
|
2560
|
+
this.loadCommitList = options instanceof GitRunner ? (range, filter, listOptions) => listCommits(options, range, filter, listOptions) : options.loadCommits ?? options.commitsLoader ?? (runner === undefined ? async () => [] : (range, filter, listOptions) => listCommits(runner, range, filter, listOptions));
|
|
2498
2561
|
this.loadCommitDetails = options instanceof GitRunner ? (oid) => loadCommit(options, oid) : options.loadCommit ?? options.commitLoader ?? (runner === undefined ? async () => {
|
|
2499
2562
|
throw new Error("Commit details require a GitRunner");
|
|
2500
2563
|
} : (oid) => loadCommit(runner, oid));
|
|
@@ -2732,6 +2795,7 @@ class AppController {
|
|
|
2732
2795
|
await this.switchLocalBranch(branch);
|
|
2733
2796
|
}
|
|
2734
2797
|
async switchLocalBranch(branch) {
|
|
2798
|
+
this.limitCommits = true;
|
|
2735
2799
|
this.logAction(LOG_ACTIONS.checkoutBranch);
|
|
2736
2800
|
await this.runBranchMutation(() => this.requireRunnerOperation((runner) => switchLocal(runner, branch)));
|
|
2737
2801
|
}
|
|
@@ -3103,7 +3167,20 @@ class AppController {
|
|
|
3103
3167
|
return this.loadCommitDetails(oid);
|
|
3104
3168
|
}
|
|
3105
3169
|
async loadBranchCommits(branch) {
|
|
3106
|
-
return this.loadCommitList(`refs/heads/${branch}
|
|
3170
|
+
return this.loadCommitList(`refs/heads/${branch}`, undefined, { limit: this.limitCommits });
|
|
3171
|
+
}
|
|
3172
|
+
async expandCommits() {
|
|
3173
|
+
if (!this.limitCommits)
|
|
3174
|
+
return false;
|
|
3175
|
+
this.limitCommits = false;
|
|
3176
|
+
const history = await this.loadCommitHistory("HEAD");
|
|
3177
|
+
this.currentState = {
|
|
3178
|
+
...this.currentState,
|
|
3179
|
+
commits: history.commits,
|
|
3180
|
+
...this.commandLogSnapshot(),
|
|
3181
|
+
...history.warning === undefined ? {} : { banner: history.warning }
|
|
3182
|
+
};
|
|
3183
|
+
return true;
|
|
3107
3184
|
}
|
|
3108
3185
|
async loadCommitFileInspection(oid, path) {
|
|
3109
3186
|
return this.loadCommitFile(oid, path);
|
|
@@ -3402,7 +3479,7 @@ class AppController {
|
|
|
3402
3479
|
}
|
|
3403
3480
|
async loadCommitHistory(range) {
|
|
3404
3481
|
try {
|
|
3405
|
-
return { commits: await this.loadCommitList(range) };
|
|
3482
|
+
return { commits: await this.loadCommitList(range, undefined, { limit: this.limitCommits }) };
|
|
3406
3483
|
} catch (error) {
|
|
3407
3484
|
const warning = error instanceof GitCommandError ? error.record.stderr || error.message : error instanceof Error ? error.message : String(error);
|
|
3408
3485
|
return { commits: this.currentState.commits ?? [], warning };
|
|
@@ -5896,6 +5973,7 @@ function reflogRows(model, filter = "") {
|
|
|
5896
5973
|
|
|
5897
5974
|
// src/ui/panes/commits-pane.ts
|
|
5898
5975
|
var paneStates = new WeakMap;
|
|
5976
|
+
var COMMIT_THRESHOLD = 200;
|
|
5899
5977
|
function formatRelativeTime(authoredAt, now) {
|
|
5900
5978
|
const date = new Date(authoredAt);
|
|
5901
5979
|
if (Number.isNaN(date.getTime()))
|
|
@@ -10212,6 +10290,7 @@ class RootView {
|
|
|
10212
10290
|
onInspectStash;
|
|
10213
10291
|
onBrowseRemote;
|
|
10214
10292
|
loadBranchCommits;
|
|
10293
|
+
onExpandCommits;
|
|
10215
10294
|
onInspectBranch;
|
|
10216
10295
|
onCheckoutRemoteTracking;
|
|
10217
10296
|
onFilterBranches;
|
|
@@ -10305,6 +10384,7 @@ class RootView {
|
|
|
10305
10384
|
this.loadTagInspection = options.loadTagInspection;
|
|
10306
10385
|
this.loadRefLogInspection = options.loadRefLogInspection;
|
|
10307
10386
|
this.loadBranchCommits = options.loadBranchCommits;
|
|
10387
|
+
this.onExpandCommits = options.onExpandCommits;
|
|
10308
10388
|
this.onPreviewError = options.onPreviewError;
|
|
10309
10389
|
this.onCommitMessage = options.onCommitMessage;
|
|
10310
10390
|
this.onAmendMessage = options.onAmendMessage;
|
|
@@ -10495,7 +10575,7 @@ class RootView {
|
|
|
10495
10575
|
}
|
|
10496
10576
|
this.branchActionGeneration += 1;
|
|
10497
10577
|
this.invalidateBranchCommitsRequest();
|
|
10498
|
-
if (!options.preserveRemoteCheckout) {
|
|
10578
|
+
if (!options.preserveRemoteCheckout && !options.preserveFilterInput) {
|
|
10499
10579
|
this.branchFilterActive = false;
|
|
10500
10580
|
this.branchFilter = "";
|
|
10501
10581
|
this.filterInput.clear();
|
|
@@ -11686,6 +11766,7 @@ class RootView {
|
|
|
11686
11766
|
this.renderCommitsPane();
|
|
11687
11767
|
this.revealListRow("commits", this.panes.commits, nextView.selectedIndex);
|
|
11688
11768
|
this.syncPreviewForFocus("commits");
|
|
11769
|
+
this.maybeExpandCommits(nextView.selectedIndex);
|
|
11689
11770
|
}
|
|
11690
11771
|
}
|
|
11691
11772
|
return;
|
|
@@ -11808,11 +11889,63 @@ class RootView {
|
|
|
11808
11889
|
}
|
|
11809
11890
|
return;
|
|
11810
11891
|
}
|
|
11811
|
-
|
|
11812
|
-
const
|
|
11813
|
-
|
|
11814
|
-
|
|
11815
|
-
|
|
11892
|
+
this.clearTransientMenus();
|
|
11893
|
+
const paneId = this.focusManager.active;
|
|
11894
|
+
if (paneId !== "files" && paneId !== "branches" && paneId !== "commits" && paneId !== "stash")
|
|
11895
|
+
return;
|
|
11896
|
+
if (paneId === "commits" && edge === "bottom") {
|
|
11897
|
+
this.jumpCommitsToBottom();
|
|
11898
|
+
return;
|
|
11899
|
+
}
|
|
11900
|
+
const active = this.activeListView(paneId);
|
|
11901
|
+
if (active === undefined || active.state.rows.length === 0)
|
|
11902
|
+
return;
|
|
11903
|
+
const rows = active.state.rows;
|
|
11904
|
+
const targetId = (edge === "bottom" ? rows[rows.length - 1] : rows[0]).id;
|
|
11905
|
+
const next = this.edgeSelection(active.state, targetId);
|
|
11906
|
+
if (next === active.state)
|
|
11907
|
+
return;
|
|
11908
|
+
this.updateActiveListState(paneId, next);
|
|
11909
|
+
this.renderListPane(paneId);
|
|
11910
|
+
this.revealListRow(paneId, this.panes[paneId], next.selectedIndex);
|
|
11911
|
+
this.syncListSelectionAfterChange(paneId);
|
|
11912
|
+
this.root.requestRender();
|
|
11913
|
+
}
|
|
11914
|
+
edgeSelection(state, targetId) {
|
|
11915
|
+
const direct = selectListRow(state, targetId);
|
|
11916
|
+
if (direct === state)
|
|
11917
|
+
return state;
|
|
11918
|
+
if (state.rangeMode !== "sticky" || state.rangeStartId === undefined)
|
|
11919
|
+
return direct;
|
|
11920
|
+
if (!state.rows.some((row) => row.id === state.rangeStartId))
|
|
11921
|
+
return direct;
|
|
11922
|
+
return { ...direct, rangeMode: state.rangeMode, rangeStartId: state.rangeStartId };
|
|
11923
|
+
}
|
|
11924
|
+
jumpCommitsToBottom() {
|
|
11925
|
+
(async () => {
|
|
11926
|
+
await this.onExpandCommits?.();
|
|
11927
|
+
const active = this.activeListView("commits");
|
|
11928
|
+
if (active === undefined || active.state.rows.length === 0)
|
|
11929
|
+
return;
|
|
11930
|
+
const rows = active.state.rows;
|
|
11931
|
+
const next = this.edgeSelection(active.state, rows[rows.length - 1].id);
|
|
11932
|
+
if (next === active.state)
|
|
11933
|
+
return;
|
|
11934
|
+
this.updateActiveListState("commits", next);
|
|
11935
|
+
this.renderListPane("commits");
|
|
11936
|
+
this.revealListRow("commits", this.panes.commits, next.selectedIndex);
|
|
11937
|
+
this.syncListSelectionAfterChange("commits");
|
|
11938
|
+
this.root.requestRender();
|
|
11939
|
+
})();
|
|
11940
|
+
}
|
|
11941
|
+
maybeExpandCommits(selectedIndex) {
|
|
11942
|
+
if (this.commitsPanel.activeTab !== "commits")
|
|
11943
|
+
return;
|
|
11944
|
+
if (this.commitsPanel.child !== undefined)
|
|
11945
|
+
return;
|
|
11946
|
+
if (selectedIndex <= COMMIT_THRESHOLD)
|
|
11947
|
+
return;
|
|
11948
|
+
this.onExpandCommits?.();
|
|
11816
11949
|
}
|
|
11817
11950
|
actionInspect() {
|
|
11818
11951
|
switch (this.focusManager.active) {
|
|
@@ -13162,6 +13295,9 @@ class RootView {
|
|
|
13162
13295
|
const existing = this.getFilterForKey(target.key);
|
|
13163
13296
|
if (existing.length > 0)
|
|
13164
13297
|
this.filters.delete(target.key);
|
|
13298
|
+
if (target.key === this.filterKey("commits", "commits")) {
|
|
13299
|
+
this.onExpandCommits?.();
|
|
13300
|
+
}
|
|
13165
13301
|
this.filterInput.open("");
|
|
13166
13302
|
this.refreshForFilterKey(target.key);
|
|
13167
13303
|
this.renderForFilterKey(target.key);
|
|
@@ -15282,7 +15418,7 @@ async function loadRefsSnapshot(runner) {
|
|
|
15282
15418
|
}
|
|
15283
15419
|
|
|
15284
15420
|
// src/git/editor.ts
|
|
15285
|
-
import { join as
|
|
15421
|
+
import { join as join5, basename } from "node:path";
|
|
15286
15422
|
function standardTerminalPreset(editor) {
|
|
15287
15423
|
return {
|
|
15288
15424
|
edit: `${editor} -- {{filename}}`,
|
|
@@ -15420,7 +15556,7 @@ async function resolveEditCommand(files, options = {}) {
|
|
|
15420
15556
|
function absolutePath(repoRoot, relativePath) {
|
|
15421
15557
|
if (relativePath.startsWith("/"))
|
|
15422
15558
|
return relativePath;
|
|
15423
|
-
return
|
|
15559
|
+
return join5(repoRoot, relativePath);
|
|
15424
15560
|
}
|
|
15425
15561
|
|
|
15426
15562
|
// src/app/create-app.ts
|
|
@@ -15428,7 +15564,7 @@ import { isAbsolute as isAbsolute2, resolve as resolve3 } from "node:path";
|
|
|
15428
15564
|
|
|
15429
15565
|
// src/app/index-watcher.ts
|
|
15430
15566
|
import { watch, statSync } from "node:fs";
|
|
15431
|
-
import { basename as basename2, dirname as
|
|
15567
|
+
import { basename as basename2, dirname as dirname4 } from "node:path";
|
|
15432
15568
|
var DEFAULT_INDEX_EVENT_DEBOUNCE_MS = 50;
|
|
15433
15569
|
var BUSY_RETRY_MS = 50;
|
|
15434
15570
|
function fingerprint(path) {
|
|
@@ -15469,7 +15605,7 @@ class IndexWatcher {
|
|
|
15469
15605
|
this.stopped = false;
|
|
15470
15606
|
this.baseline = fingerprint(this.options.indexPath);
|
|
15471
15607
|
try {
|
|
15472
|
-
const watcher = watch(
|
|
15608
|
+
const watcher = watch(dirname4(this.options.indexPath), { persistent: false }, (_eventType, filename) => {
|
|
15473
15609
|
if (this.stopped)
|
|
15474
15610
|
return;
|
|
15475
15611
|
const name = eventFileName(filename);
|
|
@@ -16135,7 +16271,7 @@ function reviewHelp(focus, state) {
|
|
|
16135
16271
|
}
|
|
16136
16272
|
|
|
16137
16273
|
// src/ui/review-workspace/review-sidebar.ts
|
|
16138
|
-
import { basename as basename3, dirname as
|
|
16274
|
+
import { basename as basename3, dirname as dirname5 } from "node:path/posix";
|
|
16139
16275
|
function normalizeDiffPath(p) {
|
|
16140
16276
|
return p?.replace(/[\r\n]+$/u, "");
|
|
16141
16277
|
}
|
|
@@ -16221,7 +16357,7 @@ function buildReviewSidebarEntries(state) {
|
|
|
16221
16357
|
let activeGroup;
|
|
16222
16358
|
visible.forEach((file, index) => {
|
|
16223
16359
|
const path = formatTerminalPath(normalizeDiffPath(file.path) ?? file.path);
|
|
16224
|
-
const group =
|
|
16360
|
+
const group = dirname5(path);
|
|
16225
16361
|
if (group !== activeGroup) {
|
|
16226
16362
|
activeGroup = group;
|
|
16227
16363
|
entries.push({
|
|
@@ -24006,6 +24142,7 @@ function createApp(options) {
|
|
|
24006
24142
|
repositoryRoot: options.repositoryRoot,
|
|
24007
24143
|
runner: options.runner,
|
|
24008
24144
|
...pullRequestLoader === undefined ? {} : { loadPullRequests: pullRequestLoader },
|
|
24145
|
+
...options.loadCommits === undefined ? {} : { loadCommits: options.loadCommits },
|
|
24009
24146
|
onPullRequestsChanged: (state) => {
|
|
24010
24147
|
renderPullRequests?.(state);
|
|
24011
24148
|
}
|
|
@@ -24285,6 +24422,12 @@ function createApp(options) {
|
|
|
24285
24422
|
},
|
|
24286
24423
|
loadCommitInspection: (oid) => controller.loadCommitInspection(oid),
|
|
24287
24424
|
loadBranchCommits: options.loadBranchCommits ?? ((branch) => controller.loadBranchCommits(branch)),
|
|
24425
|
+
onExpandCommits: async () => {
|
|
24426
|
+
const expanded = await controller.expandCommits();
|
|
24427
|
+
if (expanded && (screenController?.shouldRenderRepository() ?? true))
|
|
24428
|
+
view.update(controller.state, { preserveFilterInput: true });
|
|
24429
|
+
return expanded;
|
|
24430
|
+
},
|
|
24288
24431
|
loadCommitFileInspection: (oid, path) => controller.loadCommitFileInspection(oid, path),
|
|
24289
24432
|
loadTagInspection: (tag) => controller.loadTagInspection(tag),
|
|
24290
24433
|
loadRefLogInspection: (target) => controller.loadRefLogInspection(target),
|
|
@@ -24651,8 +24794,9 @@ function createApp(options) {
|
|
|
24651
24794
|
function shouldQueryTerminalPalette(env = process.env, capabilities) {
|
|
24652
24795
|
return env.ZELLIJ === undefined && env.ZELLIJ_SESSION_NAME === undefined && env.TERM_PROGRAM?.toLowerCase() !== "zellij" && capabilities?.multiplexer !== "zellij";
|
|
24653
24796
|
}
|
|
24654
|
-
async function startApp() {
|
|
24655
|
-
const
|
|
24797
|
+
async function startApp(options = {}) {
|
|
24798
|
+
const startDirectory = options.startDirectory === undefined ? process.cwd() : resolve4(process.cwd(), options.startDirectory);
|
|
24799
|
+
const runner = new GitRunner(startDirectory);
|
|
24656
24800
|
let repositoryRoot;
|
|
24657
24801
|
try {
|
|
24658
24802
|
repositoryRoot = (await runner.run(["rev-parse", "--show-toplevel"], { readOnly: true })).stdout.trim();
|
|
@@ -24697,4 +24841,17 @@ ${detail}
|
|
|
24697
24841
|
if (false) {}
|
|
24698
24842
|
|
|
24699
24843
|
// src/cli.ts
|
|
24700
|
-
|
|
24844
|
+
var result = parseCliArgs(process.argv.slice(2));
|
|
24845
|
+
if (result.kind === "help" || result.kind === "version") {
|
|
24846
|
+
process.stdout.write(result.text.endsWith(`
|
|
24847
|
+
`) ? result.text : `${result.text}
|
|
24848
|
+
`);
|
|
24849
|
+
process.exitCode = 0;
|
|
24850
|
+
} else if (result.kind === "error") {
|
|
24851
|
+
process.stderr.write(result.message.endsWith(`
|
|
24852
|
+
`) ? result.message : `${result.message}
|
|
24853
|
+
`);
|
|
24854
|
+
process.exitCode = result.exitCode;
|
|
24855
|
+
} else {
|
|
24856
|
+
process.exitCode = await startApp(result.startDirectory === undefined ? {} : { startDirectory: result.startDirectory });
|
|
24857
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xuhaojun/githunk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"@opentui/core": "0.5.10",
|
|
53
53
|
"@opentui/react": "0.5.10",
|
|
54
54
|
"@pierre/diffs": "1.3.5",
|
|
55
|
+
"commander": "^15.0.0",
|
|
55
56
|
"react": "19.2.8",
|
|
56
57
|
"zod": "^4.4.3"
|
|
57
58
|
},
|