@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 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}`, range];
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 ?? join(this.runner.cwd, ".git", this.relativePath);
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 : join(this.runner.cwd, 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(dirname(path), { recursive: true, mode: 448 });
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(dirname(path), "r");
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(dirname(path), { recursive: true, mode: 448 });
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(dirname(path), "r");
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 dirname2, join as join2 } from "node:path";
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 : dirname2(repoGitDirPath);
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(join2(gitDir, directory, "head-name"));
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(join2(gitDir, "BISECT_START"));
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 join3, resolve as resolve2 } from "node:path";
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(join3(directory, ".gitmodules"), "utf8");
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 : join3(worktreePath, submoduleFullPath(parentModule));
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
- const direction = edge === "bottom" ? "next" : "previous";
11812
- const branchCount = this.branchesPanel.child !== undefined ? this.branchesPanel.child.view.rows.length : this.branchesPanel.views[this.branchesPanel.activeTab]?.rows.length ?? 0;
11813
- const limit = Math.max(this.model.files.length, (this.model.worktrees ?? []).length, (this.model.submodules ?? []).length, (this.model.commits ?? []).length, (this.model.stashes ?? []).length, (this.model.reflog ?? []).length, branchCount) + 1;
11814
- for (let moved = 0;moved < limit; moved += 1)
11815
- this.actionMoveCursor(direction);
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 join4, basename } from "node:path";
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 join4(repoRoot, relativePath);
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 dirname3 } from "node:path";
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(dirname3(this.options.indexPath), { persistent: false }, (_eventType, filename) => {
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 dirname4 } from "node:path/posix";
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 = dirname4(path);
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 runner = new GitRunner;
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
- process.exitCode = await startApp();
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.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
  },