@straiffi/archon 1.8.7 → 1.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/client/assets/{ActivityView-D4Qh96zr.js → ActivityView--4X_m6mR.js} +1 -1
  2. package/dist/client/assets/{GitHubActionsDialog-Bg03OmVv.js → GitHubActionsDialog-DZ1lHM01.js} +1 -1
  3. package/dist/client/assets/{RepositoryGraphDialog-CS_hOqPw.js → RepositoryGraphDialog-B--YlQcV.js} +1 -1
  4. package/dist/client/assets/{VerificationDialog-BYH-Ek8n.js → VerificationDialog-BIWPtBg3.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-3BPJPVTR-2lxabjSl.js → architectureDiagram-3BPJPVTR-CY5xTOZJ.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-GPEHLZMM-C80HvJmb.js → blockDiagram-GPEHLZMM-DkMi7gMh.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-AAUBKEIU-BM3k_lKz.js → c4Diagram-AAUBKEIU-ZZxXBBU4.js} +1 -1
  8. package/dist/client/assets/channel-b2AN-U3x.js +1 -0
  9. package/dist/client/assets/{chunk-2J33WTMH-BTk3lX07.js → chunk-2J33WTMH-C9PiKMs-.js} +1 -1
  10. package/dist/client/assets/{chunk-3OPIFGDE-DAxdBZ_4.js → chunk-3OPIFGDE-B81aUmvi.js} +1 -1
  11. package/dist/client/assets/{chunk-5ZQYHXKU-BYm9s62a.js → chunk-5ZQYHXKU-Bk2Owm90.js} +1 -1
  12. package/dist/client/assets/{chunk-727SXJPM-iVUM8xuV.js → chunk-727SXJPM-Bx-79aJk.js} +1 -1
  13. package/dist/client/assets/{chunk-AQP2D5EJ-DVM7riMJ.js → chunk-AQP2D5EJ-GQNXYOf0.js} +1 -1
  14. package/dist/client/assets/{chunk-CSCIHK7Q-_D1FzP9B.js → chunk-CSCIHK7Q-BV22j6vn.js} +1 -1
  15. package/dist/client/assets/{chunk-KSCS5N6A-D3XouAHf.js → chunk-KSCS5N6A-C9LlcbWv.js} +1 -1
  16. package/dist/client/assets/{chunk-L5ZTLDWV-B7NYWgT4.js → chunk-L5ZTLDWV-D8PJqJnh.js} +1 -1
  17. package/dist/client/assets/{chunk-LZXEDZCA-Dgkkbb6A.js → chunk-LZXEDZCA-DcbPPozR.js} +2 -2
  18. package/dist/client/assets/{chunk-ND2GUHAM-CzO1oVCi.js → chunk-ND2GUHAM-NV-_2KqZ.js} +1 -1
  19. package/dist/client/assets/{chunk-NZK2D7GU-re0VVZne.js → chunk-NZK2D7GU-CbqA26gb.js} +1 -1
  20. package/dist/client/assets/{chunk-O5CBEL6O-BwsS59iy.js → chunk-O5CBEL6O-B0DMN44B.js} +1 -1
  21. package/dist/client/assets/{chunk-WU5MYG2G-CLyh-xCA.js → chunk-WU5MYG2G-CYpcnwf9.js} +1 -1
  22. package/dist/client/assets/classDiagram-4FO5ZUOK-C0exS5oM.js +1 -0
  23. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DkB9dQZ1.js +1 -0
  24. package/dist/client/assets/{dagre-BM42HDAG-x71fYlqQ.js → dagre-BM42HDAG-B5HD9fyK.js} +1 -1
  25. package/dist/client/assets/{diagram-2AECGRRQ-CMdwzuYj.js → diagram-2AECGRRQ-BCXvWFpM.js} +1 -1
  26. package/dist/client/assets/{diagram-5GNKFQAL-B3l6I4qJ.js → diagram-5GNKFQAL-CSpWGfAO.js} +1 -1
  27. package/dist/client/assets/{diagram-KO2AKTUF-BNhypLBW.js → diagram-KO2AKTUF-CFECGppT.js} +1 -1
  28. package/dist/client/assets/{diagram-LMA3HP47-CmrhbiDt.js → diagram-LMA3HP47-Dpb99QbZ.js} +1 -1
  29. package/dist/client/assets/{diagram-OG6HWLK6-CRobDMLl.js → diagram-OG6HWLK6-DJcTGnx0.js} +1 -1
  30. package/dist/client/assets/{erDiagram-TEJ5UH35-CqO7K0TS.js → erDiagram-TEJ5UH35-jWArJxNU.js} +1 -1
  31. package/dist/client/assets/{flowDiagram-I6XJVG4X-CuLTJZU0.js → flowDiagram-I6XJVG4X-DDF3P1hY.js} +1 -1
  32. package/dist/client/assets/{ganttDiagram-6RSMTGT7-Bm7v1eRG.js → ganttDiagram-6RSMTGT7-Corwr_18.js} +1 -1
  33. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CNHZr-5i.js → gitGraphDiagram-PVQCEYII-WzlpszWn.js} +1 -1
  34. package/dist/client/assets/index-Dy-7h3lN.js +274 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-z27pUr2X.js → infoDiagram-5YYISTIA-C0VwTwNC.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CDocZ0jf.js → ishikawaDiagram-YF4QCWOH-m9l3k2qX.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-CgP31lsU.js → journeyDiagram-JHISSGLW-DMhT8HTW.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-DxTm1_fc.js → kanban-definition-UN3LZRKU-C2ZkcOi0.js} +1 -1
  39. package/dist/client/assets/{line-hrZ7m44_.js → line-gKkUdUWR.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-BThJAIK-.js → mermaid-parser.core-DdBfJWbP.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-CfVyP_kK.js → mermaid.core-DXnhq-eu.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-CuISnLgW.js → mindmap-definition-RKZ34NQL-B0gX4aJy.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-DnWAdd44.js → pieDiagram-4H26LBE5-BHYdLmpx.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Cb-RVl5A.js → quadrantDiagram-W4KKPZXB-C-H-yQwU.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DrXg_gCS.js → requirementDiagram-4Y6WPE33-DY_pSCFy.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DzZjp_ZP.js → sankeyDiagram-5OEKKPKP-CC61HRGf.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-CQJ3PhQD.js → sequenceDiagram-3UESZ5HK-B8fXWNxw.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-B16j9pg1.js → stateDiagram-AJRCARHV-C_VYKsXW.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ctk1r914.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-bUG3mZQt.js → timeline-definition-PNZ67QCA-0fB4PIIX.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-DALCJcZD.js → vennDiagram-CIIHVFJN-CfpVF7Aa.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DPQd3l9A.js → wardleyDiagram-YWT4CUSO-CgdK4FcC.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-BIv1FqXG.js → xychartDiagram-2RQKCTM6-BEfatQYR.js} +1 -1
  54. package/dist/client/index.html +1 -1
  55. package/dist/server/index.js +41 -36
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/lib/git.js +302 -11
  58. package/dist/server/lib/git.js.map +1 -1
  59. package/dist/server/lib/integrations/github.js +135 -65
  60. package/dist/server/lib/integrations/github.js.map +1 -1
  61. package/dist/server/lib/integrations/importedIssueImages.js +10 -15
  62. package/dist/server/lib/integrations/importedIssueImages.js.map +1 -1
  63. package/package.json +1 -1
  64. package/dist/client/assets/channel-HjENOCZ0.js +0 -1
  65. package/dist/client/assets/classDiagram-4FO5ZUOK-DnhdaVrK.js +0 -1
  66. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C-BjeSGJ.js +0 -1
  67. package/dist/client/assets/index-LGNsdNYM.js +0 -274
  68. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-vNiFfHwm.js +0 -1
@@ -1,7 +1,7 @@
1
1
  import { execFile, execFileSync } from 'child_process';
2
2
  import { createHash } from 'crypto';
3
3
  import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, rmSync, statSync, symlinkSync } from 'fs';
4
- import { cp as cpAsync, lstat as lstatAsync, mkdir as mkdirAsync, symlink as symlinkAsync } from 'fs/promises';
4
+ import { cp as cpAsync, lstat as lstatAsync, mkdir as mkdirAsync, stat as statAsync, symlink as symlinkAsync } from 'fs/promises';
5
5
  import { tmpdir } from 'os';
6
6
  import { dirname, isAbsolute, join, relative, resolve } from 'path';
7
7
  import db from '../db.js';
@@ -11,6 +11,8 @@ export class UnpushedCommitDiffSelectionError extends Error {
11
11
  }
12
12
  const PROJECT_BRANCH_REMOTE_REFRESH_TTL_MS = 30_000;
13
13
  const MAX_GIT_OUTPUT_BYTES = 512 * 1024 * 1024;
14
+ const GIT_READ_TIMEOUT_MS = 15_000;
15
+ const MAX_CONCURRENT_DIFF_PROCESSES = 8;
14
16
  const lastProjectBranchRemoteRefreshAt = new Map();
15
17
  const resolveCurrentBranchName = (cwd) => {
16
18
  try {
@@ -85,12 +87,13 @@ const runGitDiffText = (args, cwd) => {
85
87
  throw error;
86
88
  }
87
89
  };
88
- const runGitTextAsync = (args, cwd) => new Promise((resolvePromise, reject) => {
90
+ const runGitTextAsync = (args, cwd, options = {}) => new Promise((resolvePromise, reject) => {
89
91
  execFile('git', args, {
90
92
  cwd,
91
93
  encoding: 'utf8',
92
94
  env: createWorkspaceProcessEnvironment(cwd),
93
95
  maxBuffer: MAX_GIT_OUTPUT_BYTES,
96
+ timeout: options.timeoutMs,
94
97
  }, (error, stdout, stderr) => {
95
98
  if (error) {
96
99
  const failed = error;
@@ -110,13 +113,26 @@ const runGitTextAsync = (args, cwd) => new Promise((resolvePromise, reject) => {
110
113
  });
111
114
  });
112
115
  });
116
+ const mapWithConcurrency = async (inputs, concurrency, operation) => {
117
+ const results = new Array(inputs.length);
118
+ let nextIndex = 0;
119
+ const worker = async () => {
120
+ while (nextIndex < inputs.length) {
121
+ const index = nextIndex;
122
+ nextIndex += 1;
123
+ results[index] = await operation(inputs[index]);
124
+ }
125
+ };
126
+ await Promise.all(Array.from({ length: Math.min(concurrency, inputs.length) }, worker));
127
+ return results;
128
+ };
113
129
  const isGitOutputLimitError = (error) => {
114
130
  const failed = error;
115
131
  return failed.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' || failed.message.includes('maxBuffer');
116
132
  };
117
- const runGitDiffTextAsync = async (args, cwd) => {
133
+ const runGitDiffTextAsync = async (args, cwd, options = {}) => {
118
134
  try {
119
- return (await runGitTextAsync(args, cwd)).stdout;
135
+ return (await runGitTextAsync(args, cwd, options)).stdout;
120
136
  }
121
137
  catch (error) {
122
138
  const failed = error;
@@ -869,6 +885,33 @@ const getWorkspaceSignature = (output, cwd, entries) => {
869
885
  .join('\0');
870
886
  return createTextSignature(`${output}\0${fileFingerprints}`);
871
887
  };
888
+ const getWorkspaceSignatureAsync = async (output, cwd, entries) => {
889
+ const sortedEntries = [...entries]
890
+ .sort((left, right) => left.path.localeCompare(right.path) || (left.originalPath ?? '').localeCompare(right.originalPath ?? ''));
891
+ const fileFingerprints = (await Promise.all(sortedEntries.map(async (entry) => {
892
+ try {
893
+ const entryPath = resolve(cwd, entry.path);
894
+ const stat = await statAsync(entryPath);
895
+ const kind = stat.isDirectory() ? 'dir' : 'file';
896
+ return `${entry.path}:${kind}:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
897
+ }
898
+ catch {
899
+ return `${entry.path}:missing`;
900
+ }
901
+ }))).map((fingerprint, index) => {
902
+ const entry = sortedEntries[index];
903
+ return [
904
+ entry.path,
905
+ entry.originalPath ?? '',
906
+ entry.status,
907
+ entry.staged ? '1' : '0',
908
+ entry.unstaged ? '1' : '0',
909
+ entry.conflicted ? '1' : '0',
910
+ fingerprint,
911
+ ].join('\u0001');
912
+ }).join('\0');
913
+ return createTextSignature(`${output}\0${fileFingerprints}`);
914
+ };
872
915
  const parseGitStatusOutput = (output) => {
873
916
  const entries = output.split('\0').filter(Boolean);
874
917
  const parsedEntries = [];
@@ -948,7 +991,7 @@ export const listGitSnapshotPaths = (cwd) => {
948
991
  return [...new Set(paths.filter(Boolean))];
949
992
  };
950
993
  const listUntrackedFilesAsync = async (cwd) => {
951
- return (await runGitTextAsync(['ls-files', '--others', '--exclude-standard'], cwd)).stdout
994
+ return (await runGitTextAsync(['ls-files', '--others', '--exclude-standard'], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS })).stdout
952
995
  .split('\n')
953
996
  .map(line => line.trim())
954
997
  .filter(Boolean);
@@ -965,7 +1008,7 @@ const getUntrackedNumstatEntry = (path, cwd) => {
965
1008
  };
966
1009
  };
967
1010
  const getUntrackedNumstatEntryAsync = async (path, cwd) => {
968
- const output = await runGitDiffTextAsync(['diff', '--no-index', '--numstat', '--', '/dev/null', path], cwd);
1011
+ const output = await runGitDiffTextAsync(['diff', '--no-index', '--numstat', '--', '/dev/null', path], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS });
969
1012
  const entry = parseNumstatOutput(output)[0];
970
1013
  if (!entry) {
971
1014
  return null;
@@ -979,7 +1022,7 @@ const getUntrackedUnifiedDiff = (path, cwd) => {
979
1022
  return runGitDiffText(['diff', '--no-index', '-U3', '--', '/dev/null', path], cwd);
980
1023
  };
981
1024
  const getUntrackedUnifiedDiffAsync = (path, cwd) => {
982
- return runGitDiffTextAsync(['diff', '--no-index', '-U3', '--', '/dev/null', path], cwd);
1025
+ return runGitDiffTextAsync(['diff', '--no-index', '-U3', '--', '/dev/null', path], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS });
983
1026
  };
984
1027
  const getSelectedDiffDataForCwd = (cwd, selectedPaths) => {
985
1028
  const normalizedPaths = [...new Set(selectedPaths.map(path => path.trim()).filter(Boolean))];
@@ -1065,12 +1108,12 @@ const getDiffDataForCwd = (cwd) => {
1065
1108
  };
1066
1109
  const getDiffDataForCwdAsync = async (cwd) => {
1067
1110
  try {
1068
- const trackedNumstat = parseNumstatOutput((await runGitTextAsync(['diff', '--numstat', '--find-renames', '--no-ext-diff', 'HEAD'], cwd)).stdout);
1111
+ const trackedNumstat = parseNumstatOutput((await runGitTextAsync(['diff', '--numstat', '--find-renames', '--no-ext-diff', 'HEAD'], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS })).stdout);
1069
1112
  const untrackedPaths = await listUntrackedFilesAsync(cwd);
1070
- const untrackedNumstat = (await Promise.all(untrackedPaths.map(path => getUntrackedNumstatEntryAsync(path, cwd))))
1113
+ const untrackedNumstat = (await mapWithConcurrency(untrackedPaths, MAX_CONCURRENT_DIFF_PROCESSES, path => getUntrackedNumstatEntryAsync(path, cwd)))
1071
1114
  .filter((entry) => entry !== null);
1072
- const trackedDiff = (await runGitTextAsync(['diff', '-U3', '--find-renames', '--no-ext-diff', 'HEAD'], cwd)).stdout;
1073
- const untrackedDiffs = await Promise.all(untrackedPaths.map(path => getUntrackedUnifiedDiffAsync(path, cwd)));
1115
+ const trackedDiff = (await runGitTextAsync(['diff', '-U3', '--find-renames', '--no-ext-diff', 'HEAD'], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS })).stdout;
1116
+ const untrackedDiffs = await mapWithConcurrency(untrackedPaths, MAX_CONCURRENT_DIFF_PROCESSES, path => getUntrackedUnifiedDiffAsync(path, cwd));
1074
1117
  return {
1075
1118
  files: [...trackedNumstat, ...untrackedNumstat].map(entry => ({
1076
1119
  path: entry.path,
@@ -1392,6 +1435,9 @@ export const getGitDiffFiles = (cwd) => {
1392
1435
  export const getGitDiffData = (cwd) => {
1393
1436
  return getDiffDataForCwd(cwd);
1394
1437
  };
1438
+ export const getGitDiffDataAsync = (cwd) => {
1439
+ return getDiffDataForCwdAsync(cwd);
1440
+ };
1395
1441
  export const getGitDiffFilesAsync = async (cwd) => {
1396
1442
  return (await getDiffDataForCwdAsync(cwd)).files;
1397
1443
  };
@@ -1580,6 +1626,209 @@ export const getGitStatus = (cwd, branch = null, project = null, options = {}) =
1580
1626
  };
1581
1627
  }
1582
1628
  };
1629
+ const resolveBranchUpstreamRefAsync = async (branch, cwd) => {
1630
+ try {
1631
+ const output = await runGitTextAsync(['rev-parse', '--symbolic-full-name', `${branch}@{upstream}`], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS });
1632
+ return output.stdout.trim() || null;
1633
+ }
1634
+ catch {
1635
+ return null;
1636
+ }
1637
+ };
1638
+ const getBranchSyncStateAsync = async (entries, cwd, recordedBase) => {
1639
+ const branchState = entries
1640
+ .filter(entry => entry.startsWith('# '))
1641
+ .reduce((current, entry) => parseBranchHeader(entry, current), {
1642
+ branch: null,
1643
+ ahead: 0,
1644
+ behind: 0,
1645
+ });
1646
+ if (!branchState.branch) {
1647
+ return { branch: null, ahead: 0, behind: 0, hasUpstream: false, canPush: false };
1648
+ }
1649
+ const upstreamRef = await resolveBranchUpstreamRefAsync(branchState.branch, cwd);
1650
+ let ahead = branchState.ahead;
1651
+ if (!upstreamRef && ahead === 0 && branchState.behind === 0) {
1652
+ const recordedBaseRef = recordedBase?.baseCommit ?? recordedBase?.baseBranch ?? null;
1653
+ const args = recordedBaseRef
1654
+ ? ['rev-list', '--count', `${recordedBaseRef}..HEAD`]
1655
+ : ['rev-list', '--count', 'HEAD', '--not', '--remotes'];
1656
+ try {
1657
+ const output = await runGitTextAsync(args, cwd, { timeoutMs: GIT_READ_TIMEOUT_MS });
1658
+ const parsedAhead = Number.parseInt(output.stdout.trim() || '0', 10);
1659
+ ahead = Number.isFinite(parsedAhead) ? parsedAhead : 0;
1660
+ }
1661
+ catch {
1662
+ ahead = 0;
1663
+ }
1664
+ }
1665
+ return {
1666
+ branch: branchState.branch,
1667
+ ahead,
1668
+ behind: branchState.behind,
1669
+ hasUpstream: upstreamRef !== null,
1670
+ canPush: upstreamRef !== null || ahead > 0,
1671
+ };
1672
+ };
1673
+ const getGitOperationStateAsync = async (cwd) => {
1674
+ try {
1675
+ const output = await runGitTextAsync(['rev-parse', '--git-dir'], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS });
1676
+ const gitDir = output.stdout.trim();
1677
+ if (!gitDir) {
1678
+ return 'idle';
1679
+ }
1680
+ const resolvedGitDir = resolve(cwd, gitDir);
1681
+ if (existsSync(join(resolvedGitDir, 'MERGE_HEAD'))) {
1682
+ return 'merge';
1683
+ }
1684
+ if (existsSync(join(resolvedGitDir, 'rebase-merge')) || existsSync(join(resolvedGitDir, 'rebase-apply'))) {
1685
+ return 'rebase';
1686
+ }
1687
+ }
1688
+ catch {
1689
+ return 'idle';
1690
+ }
1691
+ return 'idle';
1692
+ };
1693
+ const resolveFallbackBaseAsync = async (branch, project) => {
1694
+ const repoPath = getRepoPath(project);
1695
+ try {
1696
+ const branchOutput = await runGitTextAsync(['branch', '--show-current'], repoPath, { timeoutMs: GIT_READ_TIMEOUT_MS });
1697
+ const baseBranch = branchOutput.stdout.trim();
1698
+ if (!baseBranch || baseBranch === branch) {
1699
+ return null;
1700
+ }
1701
+ const mergeBaseOutput = await runGitTextAsync(['merge-base', baseBranch, branch], repoPath, { timeoutMs: GIT_READ_TIMEOUT_MS });
1702
+ return {
1703
+ baseBranch,
1704
+ baseCommit: mergeBaseOutput.stdout.trim() || null,
1705
+ };
1706
+ }
1707
+ catch {
1708
+ return null;
1709
+ }
1710
+ };
1711
+ const refreshProjectBranchTrackingAsync = async (cwd, branch) => {
1712
+ try {
1713
+ const upstreamRef = await resolveBranchUpstreamRefAsync(branch, cwd);
1714
+ const fetchSpec = parseRemoteBranchRef(upstreamRef);
1715
+ const args = fetchSpec
1716
+ ? ['fetch', '--quiet', fetchSpec.remote, fetchSpec.branch]
1717
+ : ['fetch', '--quiet', 'origin', branch];
1718
+ await runGitTextAsync(args, cwd, { timeoutMs: GIT_READ_TIMEOUT_MS });
1719
+ }
1720
+ catch {
1721
+ // Keep the last known tracking state when fetch is unavailable.
1722
+ }
1723
+ };
1724
+ const getUnreadableGitStatus = () => ({
1725
+ branch: null,
1726
+ ahead: 0,
1727
+ behind: 0,
1728
+ has_upstream: false,
1729
+ operation_state: 'idle',
1730
+ can_abort: false,
1731
+ can_force_push_with_lease: false,
1732
+ base_branch: null,
1733
+ pull_disabled_reason: 'Unable to read git status for this worktree.',
1734
+ rebase_disabled_reason: 'Unable to read git status for this worktree.',
1735
+ workspace_signature: null,
1736
+ is_dirty: false,
1737
+ has_staged_changes: false,
1738
+ has_unstaged_changes: false,
1739
+ files: [],
1740
+ can_commit: false,
1741
+ can_push: false,
1742
+ blocked_reason: 'Unable to read git status for this worktree.',
1743
+ });
1744
+ export const getGitStatusAsync = async (cwd, branch = null, project = null, options = {}) => {
1745
+ const recordedBase = branch && project ? resolveRecordedBaseForCwd(cwd, branch, project) : null;
1746
+ try {
1747
+ if (branch && options.refreshRemote && shouldRefreshProjectBranchTracking(getProjectBranchRefreshCacheKey(cwd, branch), true)) {
1748
+ await refreshProjectBranchTrackingAsync(cwd, branch);
1749
+ }
1750
+ const output = (await runGitTextAsync(['status', '--porcelain=v2', '--branch', '--untracked-files=all', '-z'], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS })).stdout;
1751
+ const parsedOutput = parseGitStatusOutput(output);
1752
+ const branchState = await getBranchSyncStateAsync(parsedOutput.entries, cwd, recordedBase);
1753
+ const resolvedBranch = branchState.branch ?? branch;
1754
+ const fallbackBase = !recordedBase && resolvedBranch && project
1755
+ ? await resolveFallbackBaseAsync(resolvedBranch, project)
1756
+ : null;
1757
+ const effectiveBase = recordedBase ?? fallbackBase;
1758
+ const includeFiles = options.includeFiles ?? true;
1759
+ const files = includeFiles
1760
+ ? parsedOutput.parsedEntries.map(entry => ({
1761
+ path: entry.path,
1762
+ original_path: entry.originalPath,
1763
+ status: entry.status,
1764
+ staged: entry.staged,
1765
+ unstaged: entry.unstaged,
1766
+ conflicted: entry.conflicted,
1767
+ }))
1768
+ : [];
1769
+ const blockedReason = includeFiles ? getCommitBlockedReason(files) : null;
1770
+ const operationState = await getGitOperationStateAsync(cwd);
1771
+ const baseBranch = effectiveBase?.baseBranch && effectiveBase.baseBranch !== resolvedBranch
1772
+ ? effectiveBase.baseBranch
1773
+ : null;
1774
+ const workspaceSignature = options.includeWorkspaceSignature === false
1775
+ ? null
1776
+ : await getWorkspaceSignatureAsync(output, cwd, parsedOutput.parsedEntries);
1777
+ const status = {
1778
+ branch: resolvedBranch,
1779
+ ahead: branchState.ahead,
1780
+ behind: branchState.behind,
1781
+ has_upstream: branchState.hasUpstream,
1782
+ operation_state: operationState,
1783
+ can_abort: operationState !== 'idle',
1784
+ can_force_push_with_lease: branchState.hasUpstream && branchState.ahead > 0 && branchState.behind > 0 && operationState === 'idle',
1785
+ base_branch: baseBranch,
1786
+ pull_disabled_reason: null,
1787
+ rebase_disabled_reason: null,
1788
+ workspace_signature: workspaceSignature,
1789
+ is_dirty: parsedOutput.parsedEntries.length > 0,
1790
+ has_staged_changes: parsedOutput.parsedEntries.some(entry => entry.staged),
1791
+ has_unstaged_changes: parsedOutput.parsedEntries.some(entry => entry.unstaged || entry.status === 'untracked'),
1792
+ files,
1793
+ can_commit: includeFiles && files.length > 0 && blockedReason === null,
1794
+ can_push: branchState.canPush,
1795
+ blocked_reason: blockedReason,
1796
+ };
1797
+ return {
1798
+ ...status,
1799
+ pull_disabled_reason: !status.branch
1800
+ ? 'Select a named branch before pulling.'
1801
+ : operationState !== 'idle' || status.is_dirty || !status.has_upstream
1802
+ ? getPullDisabledReasonWithoutGit(status)
1803
+ : null,
1804
+ rebase_disabled_reason: getRebaseDisabledReason(status),
1805
+ };
1806
+ }
1807
+ catch {
1808
+ return getUnreadableGitStatus();
1809
+ }
1810
+ };
1811
+ const getPullDisabledReasonWithoutGit = (status) => {
1812
+ if (!status.branch) {
1813
+ return 'Select a named branch before pulling.';
1814
+ }
1815
+ if (status.operation_state === 'merge') {
1816
+ return 'Abort the current merge before pulling again.';
1817
+ }
1818
+ if (status.operation_state === 'rebase') {
1819
+ return 'Abort the current rebase before pulling again.';
1820
+ }
1821
+ if (status.files.some(file => file.conflicted)) {
1822
+ return 'Resolve conflicted files before pulling from the UI.';
1823
+ }
1824
+ if (status.is_dirty) {
1825
+ return 'Commit or stash changes before pulling from the UI.';
1826
+ }
1827
+ if (!status.has_upstream) {
1828
+ return `Branch "${status.branch}" has no configured upstream.`;
1829
+ }
1830
+ return null;
1831
+ };
1583
1832
  export const getWorktreeGitStatus = (branch, project = null) => {
1584
1833
  const cwd = resolveWorktreePath(branch, project);
1585
1834
  return getGitStatus(cwd, branch, project);
@@ -1981,6 +2230,48 @@ export const getUnpushedCommitSummaryForCwd = (cwd, branch, project = null, opti
1981
2230
  };
1982
2231
  }
1983
2232
  };
2233
+ export const getUnpushedCommitSummaryForCwdAsync = async (cwd, branch, project = null) => {
2234
+ const recordedBase = project ? resolveRecordedBaseForCwd(cwd, branch, project) : null;
2235
+ const upstreamRef = await resolveBranchUpstreamRefAsync(branch, cwd);
2236
+ const baseRef = upstreamRef ?? recordedBase?.baseBranch ?? recordedBase?.baseCommit ?? null;
2237
+ let baseCommit = null;
2238
+ if (baseRef) {
2239
+ try {
2240
+ const output = await runGitTextAsync(['rev-parse', '--verify', `${baseRef}^{commit}`], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS });
2241
+ baseCommit = output.stdout.trim() || null;
2242
+ }
2243
+ catch {
2244
+ baseCommit = recordedBase?.baseCommit?.trim() || null;
2245
+ }
2246
+ }
2247
+ if (!baseCommit && project && resolve(cwd) === getRepoPath(project)) {
2248
+ try {
2249
+ const output = await runGitTextAsync(['rev-parse', 'HEAD'], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS });
2250
+ baseCommit = output.stdout.trim() || null;
2251
+ }
2252
+ catch {
2253
+ baseCommit = null;
2254
+ }
2255
+ }
2256
+ const rangeArgs = baseCommit ? [`${baseCommit}..HEAD`] : ['HEAD', '--not', '--remotes'];
2257
+ const [countOutput, logOutput] = await Promise.all([
2258
+ runGitTextAsync(['rev-list', '--count', ...rangeArgs], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS }),
2259
+ runGitTextAsync([
2260
+ 'log',
2261
+ '--format=%H%x00%h%x00%s%x00%cI%x00',
2262
+ '--no-show-signature',
2263
+ '-n',
2264
+ '20',
2265
+ ...rangeArgs,
2266
+ ], cwd, { timeoutMs: GIT_READ_TIMEOUT_MS }),
2267
+ ]);
2268
+ const totalCount = Number.parseInt(countOutput.stdout.trim() || '0', 10);
2269
+ return {
2270
+ commits: parseUnpushedCommitsOutput(logOutput.stdout),
2271
+ totalCount: Number.isFinite(totalCount) ? totalCount : 0,
2272
+ baseCommit,
2273
+ };
2274
+ };
1984
2275
  export const listUnpushedCommitsForCwd = (cwd, branch, project = null, options = {}) => {
1985
2276
  return getUnpushedCommitSummaryForCwd(cwd, branch, project, options).commits;
1986
2277
  };