@openchamber/web 1.11.6 → 1.11.7

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 (47) hide show
  1. package/README.md +6 -0
  2. package/bin/cli.js +443 -2
  3. package/dist/assets/{MarkdownRendererImpl-COdbjw73.js → MarkdownRendererImpl-DaF15QNC.js} +3 -3
  4. package/dist/assets/{MultiRunWindow-BKSHxjMq.js → MultiRunWindow-Cl7wS_CB.js} +1 -1
  5. package/dist/assets/{OnboardingScreen-Chjg337p.js → OnboardingScreen-DTv6YJI1.js} +2 -2
  6. package/dist/assets/{SettingsWindow-C0lRRW8M.js → SettingsWindow-_c3TTL2z.js} +1 -1
  7. package/dist/assets/{TerminalView-Bvil3j1u.js → TerminalView-CuXkDROt.js} +3 -3
  8. package/dist/assets/es-CYoUf2D-.js +15 -0
  9. package/dist/assets/{index-B9LvUHdG.js → index-3WXrN3AX.js} +1 -1
  10. package/dist/assets/index-BREIbhcb.css +1 -0
  11. package/dist/assets/ko-2tM0fIna.js +15 -0
  12. package/dist/assets/main-BF3kWAJ9.js +239 -0
  13. package/dist/assets/{main-Blhx9Fp5.js → main-o8ZERrmU.js} +2 -2
  14. package/dist/assets/miniChat-BZQjpK23.js +2 -0
  15. package/dist/assets/{modelPrefsAutoSave-DRJSYigo.js → modelPrefsAutoSave-wwnbqBk7.js} +110 -108
  16. package/dist/assets/pl-Dq8uAotM.js +15 -0
  17. package/dist/assets/pt-BR-nh9s9DFT.js +15 -0
  18. package/dist/assets/{renderElectronMiniChatApp-BxZRI73j.js → renderElectronMiniChatApp-C-Ezew9P.js} +2 -2
  19. package/dist/assets/uk-BZtz0wUV.js +15 -0
  20. package/dist/assets/{vendor-.bun-Bum-iBXX.js → vendor-.bun-CV3tusA8.js} +1 -1
  21. package/dist/assets/zh-CN-j_nYMchE.js +15 -0
  22. package/dist/assets/zh-TW-B11UpkDJ.js +15 -0
  23. package/dist/index.html +11 -28
  24. package/dist/mini-chat.html +4 -4
  25. package/package.json +1 -1
  26. package/server/lib/fs/routes.js +5 -0
  27. package/server/lib/fs/routes.test.js +61 -1
  28. package/server/lib/git/DOCUMENTATION.md +1 -0
  29. package/server/lib/git/routes.js +82 -1
  30. package/server/lib/git/service.js +338 -19
  31. package/server/lib/git/service.test.js +414 -8
  32. package/server/lib/opencode/env-runtime.js +52 -4
  33. package/server/lib/opencode/env-runtime.test.js +82 -6
  34. package/server/lib/opencode/openchamber-routes.js +9 -7
  35. package/server/lib/opencode/settings-helpers.js +3 -0
  36. package/server/lib/opencode/settings-runtime.js +39 -1
  37. package/server/lib/opencode/settings-runtime.test.js +39 -0
  38. package/server/lib/skills-catalog/source.js +1 -1
  39. package/dist/assets/es-BZIAUghG.js +0 -15
  40. package/dist/assets/index-UcCH2KN9.css +0 -1
  41. package/dist/assets/ko-DU9l-zox.js +0 -15
  42. package/dist/assets/main-d2-dY4er.js +0 -232
  43. package/dist/assets/miniChat-CJ7-rZFl.js +0 -2
  44. package/dist/assets/pl-CdqzokG-.js +0 -15
  45. package/dist/assets/pt-BR-Bknbr_Y3.js +0 -15
  46. package/dist/assets/uk-Be4E8ZNO.js +0 -15
  47. package/dist/assets/zh-CN-qpPiaZMg.js +0 -15
@@ -12,6 +12,10 @@ const execFileAsync = promisify(execFile);
12
12
  const gpgconfCandidates = ['gpgconf', '/opt/homebrew/bin/gpgconf', '/usr/local/bin/gpgconf'];
13
13
  let resolvedGitBinary = null;
14
14
  const worktreeBootstrapState = new Map();
15
+ const remoteExistenceCache = new Map();
16
+ const SIMPLE_GIT_SAFE_BINARY_PATTERN = /^([a-z]:)?([a-z0-9/.\\_~-]+)$/i;
17
+ const SIMPLE_GIT_UNSAFE_BINARY_WARNING = 'Invalid value supplied for custom binary, restricted characters must be removed';
18
+ const REMOTE_EXISTENCE_CACHE_TTL_MS = 30_000;
15
19
  const gitIndexMutationQueues = new Map();
16
20
 
17
21
  const WORKTREE_BOOTSTRAP_PENDING = 'pending';
@@ -86,6 +90,30 @@ const normalizeGitExecutableCandidate = (candidate) => {
86
90
  return trimmed;
87
91
  };
88
92
 
93
+ const isSafeSimpleGitBinary = (candidate) => (
94
+ typeof candidate === 'string' && SIMPLE_GIT_SAFE_BINARY_PATTERN.test(candidate)
95
+ );
96
+
97
+ const createSimpleGit = (options) => {
98
+ if (!options?.unsafe?.allowUnsafeCustomBinary) {
99
+ return simpleGit(options);
100
+ }
101
+
102
+ const originalWarn = console.warn;
103
+ console.warn = (...args) => {
104
+ if (String(args[0] || '').includes(SIMPLE_GIT_UNSAFE_BINARY_WARNING)) {
105
+ return;
106
+ }
107
+ originalWarn(...args);
108
+ };
109
+
110
+ try {
111
+ return simpleGit(options);
112
+ } finally {
113
+ console.warn = originalWarn;
114
+ }
115
+ };
116
+
89
117
  const listPathExecutableCandidates = (binaryName) => {
90
118
  const currentPath = process.env.PATH || '';
91
119
  const seen = new Set();
@@ -133,22 +161,34 @@ const resolveGitBinary = () => {
133
161
  .map((value) => (typeof value === 'string' ? value.trim() : ''))
134
162
  .filter(Boolean);
135
163
  for (const candidate of explicit) {
136
- if (isExecutableFile(candidate)) {
137
- resolvedGitBinary = candidate;
164
+ const normalized = normalizeGitExecutableCandidate(candidate);
165
+ if (isExecutableFile(normalized)) {
166
+ resolvedGitBinary = normalized;
138
167
  return resolvedGitBinary;
139
168
  }
140
169
  }
141
170
 
142
- const discovered = [
171
+ const pathDiscovered = [
143
172
  ...listPathExecutableCandidates('git.exe'),
144
173
  ...listPathExecutableCandidates('git'),
174
+ ]
175
+ .map(normalizeGitExecutableCandidate)
176
+ .filter(Boolean)
177
+ .filter((candidate) => isExecutableFile(candidate));
178
+ if (pathDiscovered.length > 0) {
179
+ resolvedGitBinary = 'git';
180
+ return resolvedGitBinary;
181
+ }
182
+
183
+ const discovered = [
145
184
  ...listWindowsGitInstallCandidates(),
146
185
  ]
147
186
  .map(normalizeGitExecutableCandidate)
148
187
  .filter(Boolean)
149
188
  .filter((candidate) => isExecutableFile(candidate));
150
189
 
151
- const preferredExe = discovered.find((candidate) => candidate.toLowerCase().endsWith('.exe'));
190
+ const preferredExe = discovered.find((candidate) => isSafeSimpleGitBinary(candidate) && candidate.toLowerCase().endsWith('.exe'))
191
+ || discovered.find((candidate) => candidate.toLowerCase().endsWith('.exe'));
152
192
  resolvedGitBinary = preferredExe || discovered[0] || 'git.exe';
153
193
  return resolvedGitBinary;
154
194
  };
@@ -276,9 +316,9 @@ const createGit = async (directory) => {
276
316
  const hasCustomBinary = typeof binary === 'string' && binary.trim() && binary !== 'git' && binary !== 'git.exe';
277
317
  const unsafe = hasCustomBinary ? { allowUnsafeCustomBinary: true } : undefined;
278
318
  if (!directory) {
279
- return simpleGit({ env, spawnOptions, binary, unsafe });
319
+ return createSimpleGit({ env, spawnOptions, binary, unsafe });
280
320
  }
281
- return simpleGit({
321
+ return createSimpleGit({
282
322
  baseDir: normalizeDirectoryPath(directory),
283
323
  env,
284
324
  spawnOptions,
@@ -596,6 +636,10 @@ const normalizeStartRef = (value) => {
596
636
  return trimmed;
597
637
  };
598
638
 
639
+ function isValidCommitHash(hash) {
640
+ return typeof hash === 'string' && /^[0-9a-fA-F]{7,40}$/.test(hash);
641
+ }
642
+
599
643
  const parseRemoteBranchRef = (value) => {
600
644
  const trimmed = String(value || '').trim();
601
645
  if (!trimmed) {
@@ -677,6 +721,96 @@ const parseGitErrorText = (error) => {
677
721
  .trim();
678
722
  };
679
723
 
724
+ const parseAheadBehindCounts = (value) => {
725
+ const [aheadRaw, behindRaw] = String(value || '').trim().split(/\s+/);
726
+ const ahead = parseInt(aheadRaw, 10);
727
+ const behind = parseInt(behindRaw, 10);
728
+ if (!Number.isFinite(ahead) || !Number.isFinite(behind)) {
729
+ return null;
730
+ }
731
+ return { ahead, behind };
732
+ };
733
+
734
+ const getRemoteExistenceCacheKey = (directory, remoteName) => {
735
+ const normalizedDirectory = normalizeDirectoryPath(directory) || '';
736
+ return `${path.resolve(normalizedDirectory)}\0${remoteName}`;
737
+ };
738
+
739
+ const hasRemote = async (git, directory, remoteName) => {
740
+ const remote = String(remoteName || '').trim();
741
+ if (!remote) {
742
+ return false;
743
+ }
744
+
745
+ const key = getRemoteExistenceCacheKey(directory, remote);
746
+ const cached = remoteExistenceCache.get(key);
747
+ if (cached && Date.now() - cached.checkedAt < REMOTE_EXISTENCE_CACHE_TTL_MS) {
748
+ return cached.exists;
749
+ }
750
+
751
+ const exists = await git
752
+ .raw(['remote', 'get-url', remote])
753
+ .then((value) => String(value || '').trim().length > 0)
754
+ .catch(() => false);
755
+
756
+ remoteExistenceCache.set(key, { exists, checkedAt: Date.now() });
757
+ return exists;
758
+ };
759
+
760
+ const buildRawGitOptions = (raw) => {
761
+ if (Array.isArray(raw)) {
762
+ return raw.map((value) => String(value || '').trim()).filter(Boolean);
763
+ }
764
+
765
+ if (!raw || typeof raw !== 'object') {
766
+ return [];
767
+ }
768
+
769
+ return Object.entries(raw).flatMap(([key, value]) => {
770
+ const option = String(key || '').trim();
771
+ if (!option || value === false) {
772
+ return [];
773
+ }
774
+ if (value === true || value == null) {
775
+ return [option];
776
+ }
777
+ return [option, String(value)];
778
+ });
779
+ };
780
+
781
+ const getRemoteBranchComparison = async (git, remoteName, branchName) => {
782
+ const remote = String(remoteName || '').trim();
783
+ const branch = String(branchName || '').trim();
784
+ if (!remote || !branch) {
785
+ return null;
786
+ }
787
+
788
+ const remoteRef = `refs/remotes/${remote}/${branch}`;
789
+ const exists = await git
790
+ .raw(['rev-parse', '--verify', remoteRef])
791
+ .then((value) => String(value || '').trim())
792
+ .catch(() => '');
793
+ if (!exists) {
794
+ return null;
795
+ }
796
+
797
+ const countsRaw = await git
798
+ .raw(['rev-list', '--left-right', '--count', `HEAD...${remoteRef}`])
799
+ .then((value) => String(value || '').trim())
800
+ .catch(() => '');
801
+ const counts = parseAheadBehindCounts(countsRaw);
802
+ if (!counts) {
803
+ return null;
804
+ }
805
+
806
+ return {
807
+ remote,
808
+ branch,
809
+ ahead: counts.ahead,
810
+ behind: counts.behind,
811
+ };
812
+ };
813
+
680
814
  const isNotGitRepositoryError = (error) => {
681
815
  const text = parseGitErrorText(error);
682
816
  return /not a git repository/i.test(text);
@@ -1342,7 +1476,7 @@ export async function getStatus(directory, options = {}) {
1342
1476
  const lightMode = options.mode === 'light';
1343
1477
 
1344
1478
  try {
1345
- const { repoRoot, git } = await createRepositoryGitContext(directory);
1479
+ const { directoryPath, repoRoot, git } = await createRepositoryGitContext(directory);
1346
1480
 
1347
1481
  // Use -uall to show all untracked files individually, not just directories
1348
1482
  const status = await git.status(['-uall']);
@@ -1495,6 +1629,7 @@ export async function getStatus(directory, options = {}) {
1495
1629
  let tracking = status.tracking || null;
1496
1630
  let ahead = status.ahead;
1497
1631
  let behind = status.behind;
1632
+ let upstreamComparison;
1498
1633
 
1499
1634
  // When no upstream is configured (common for new worktree branches), Git doesn't report ahead/behind.
1500
1635
  // We still want to show the number of unpublished commits to the user.
@@ -1514,6 +1649,15 @@ export async function getStatus(directory, options = {}) {
1514
1649
  }
1515
1650
  }
1516
1651
 
1652
+ if (
1653
+ !lightMode
1654
+ && status.current
1655
+ && (!tracking || !tracking.startsWith('upstream/'))
1656
+ && await hasRemote(git, directoryPath, 'upstream')
1657
+ ) {
1658
+ upstreamComparison = await getRemoteBranchComparison(git, 'upstream', status.current);
1659
+ }
1660
+
1517
1661
  // Check for in-progress operations
1518
1662
  let mergeInProgress = null;
1519
1663
  let rebaseInProgress = null;
@@ -1574,6 +1718,7 @@ export async function getStatus(directory, options = {}) {
1574
1718
  tracking,
1575
1719
  ahead,
1576
1720
  behind,
1721
+ upstreamComparison,
1577
1722
  files: status.files.map((f) => ({
1578
1723
  path: f.path,
1579
1724
  index: f.index,
@@ -1984,9 +2129,20 @@ export async function pull(directory, options = {}) {
1984
2129
  : options.options || {};
1985
2130
 
1986
2131
  try {
2132
+ const remote = String(options.remote || '').trim();
2133
+ const requestedBranch = String(options.branch || '').trim();
2134
+ let branch = requestedBranch;
2135
+
2136
+ if (remote && !branch) {
2137
+ // simple-git only includes the remote when both remote and branch are provided.
2138
+ // Resolve the current branch so selecting a remote in the UI really runs `git pull <remote> <branch>`.
2139
+ const status = await git.status();
2140
+ branch = String(status.current || '').trim();
2141
+ }
2142
+
1987
2143
  const result = await git.pull(
1988
- options.remote || 'origin',
1989
- options.branch,
2144
+ remote || 'origin',
2145
+ branch || undefined,
1990
2146
  pullOptions
1991
2147
  );
1992
2148
 
@@ -2240,11 +2396,20 @@ export async function fetch(directory, options = {}) {
2240
2396
  const { git } = await createRepositoryGitContext(directory);
2241
2397
 
2242
2398
  try {
2243
- await git.fetch(
2244
- options.remote || 'origin',
2245
- options.branch,
2246
- options.options || {}
2247
- );
2399
+ const remote = String(options.remote || '').trim();
2400
+ const branch = String(options.branch || '').trim();
2401
+ const fetchOptions = options.options || {};
2402
+
2403
+ if (remote && !branch) {
2404
+ // simple-git drops the remote when branch is omitted, so use raw to preserve `git fetch <remote>`.
2405
+ await git.raw(['fetch', ...buildRawGitOptions(fetchOptions), remote]);
2406
+ } else {
2407
+ await git.fetch(
2408
+ remote || 'origin',
2409
+ branch || undefined,
2410
+ fetchOptions
2411
+ );
2412
+ }
2248
2413
 
2249
2414
  return { success: true };
2250
2415
  } catch (error) {
@@ -2531,6 +2696,99 @@ export async function checkoutBranch(directory, branchName) {
2531
2696
  }
2532
2697
  }
2533
2698
 
2699
+ export async function checkoutCommit(directory, hash) {
2700
+ if (!isValidCommitHash(hash)) {
2701
+ throw new Error('Invalid commit hash');
2702
+ }
2703
+ const { git } = await createRepositoryGitContext(directory);
2704
+ try {
2705
+ await git.checkout(hash);
2706
+ return { success: true };
2707
+ } catch (error) {
2708
+ console.error('Failed to checkout commit:', error);
2709
+ throw error;
2710
+ }
2711
+ }
2712
+
2713
+ export async function cherryPick(directory, hash) {
2714
+ if (!isValidCommitHash(hash)) {
2715
+ throw new Error('Invalid commit hash');
2716
+ }
2717
+ const { git } = await createRepositoryGitContext(directory);
2718
+ try {
2719
+ await git.raw(['cherry-pick', hash]);
2720
+ return { success: true, conflict: false };
2721
+ } catch (error) {
2722
+ const errorMessage = String(error?.message || error || '').toLowerCase();
2723
+ const isConflict =
2724
+ errorMessage.includes('conflict') ||
2725
+ errorMessage.includes('patch does not apply');
2726
+
2727
+ if (isConflict) {
2728
+ const status = await git.status().catch(() => ({ conflicted: [] }));
2729
+ return {
2730
+ success: false,
2731
+ conflict: true,
2732
+ conflictFiles: status.conflicted || [],
2733
+ };
2734
+ }
2735
+
2736
+ console.error('Failed to cherry-pick:', error);
2737
+ throw error;
2738
+ }
2739
+ }
2740
+
2741
+ export async function revertCommit(directory, hash) {
2742
+ if (!isValidCommitHash(hash)) {
2743
+ throw new Error('Invalid commit hash');
2744
+ }
2745
+ const { git } = await createRepositoryGitContext(directory);
2746
+ try {
2747
+ await git.raw(['revert', '--no-commit', hash]);
2748
+ return { success: true, conflict: false };
2749
+ } catch (error) {
2750
+ const errorMessage = String(error?.message || error || '').toLowerCase();
2751
+ const isConflict =
2752
+ errorMessage.includes('conflict') ||
2753
+ errorMessage.includes('revert failed');
2754
+
2755
+ if (isConflict) {
2756
+ const status = await git.status().catch(() => ({ conflicted: [] }));
2757
+ return {
2758
+ success: false,
2759
+ conflict: true,
2760
+ conflictFiles: status.conflicted || [],
2761
+ };
2762
+ }
2763
+
2764
+ console.error('Failed to revert commit:', error);
2765
+ throw error;
2766
+ }
2767
+ }
2768
+
2769
+ export async function resetToCommit(directory, hash, mode, force = false) {
2770
+ if (!isValidCommitHash(hash)) {
2771
+ throw new Error('Invalid commit hash');
2772
+ }
2773
+ const { git } = await createRepositoryGitContext(directory);
2774
+
2775
+ if (mode === 'hard' && !force) {
2776
+ const status = await git.status();
2777
+ const isDirty = !status.isClean();
2778
+ if (isDirty) {
2779
+ throw new Error('Cannot hard reset: uncommitted changes in working tree. Stash or commit first, or use force.');
2780
+ }
2781
+ }
2782
+
2783
+ try {
2784
+ await git.raw(['reset', `--${mode}`, hash]);
2785
+ return { success: true };
2786
+ } catch (error) {
2787
+ console.error('Failed to reset to commit:', error);
2788
+ throw error;
2789
+ }
2790
+ }
2791
+
2534
2792
  export async function getWorktrees(directory) {
2535
2793
  const directoryPath = normalizeDirectoryPath(directory);
2536
2794
  if (!directoryPath || !fs.existsSync(directoryPath)) {
@@ -3018,6 +3276,65 @@ export async function getLog(directory, options = {}) {
3018
3276
 
3019
3277
  try {
3020
3278
  const maxCount = options.maxCount || 50;
3279
+
3280
+ if (options.all) {
3281
+ const logArgs = [
3282
+ 'log',
3283
+ `--max-count=${maxCount}`,
3284
+ '--all',
3285
+ '--topo-order',
3286
+ '--date=iso',
3287
+ '--pretty=format:%x1e%H%x1f%P%x1f%an%x1f%ae%x1f%ad%x1f%s%x1f%D',
3288
+ '--shortstat',
3289
+ ];
3290
+
3291
+ const rawLog = await git.raw(logArgs);
3292
+ const records = rawLog
3293
+ .split('\x1e')
3294
+ .map((e) => e.trim())
3295
+ .filter(Boolean);
3296
+
3297
+ const entries = [];
3298
+ for (const record of records) {
3299
+ const lines = record.split('\n').filter((l) => l.trim().length > 0);
3300
+ const header = lines.shift() || '';
3301
+ const [hash, parentsRaw, author_name, author_email, date, message, refsRaw] =
3302
+ header.split('\x1f');
3303
+ if (!hash) continue;
3304
+
3305
+ const parents = parentsRaw ? parentsRaw.trim().split(' ').filter(Boolean) : [];
3306
+ const refs = refsRaw ? refsRaw.trim() : '';
3307
+
3308
+ let filesChanged = 0;
3309
+ let insertions = 0;
3310
+ let deletions = 0;
3311
+ for (const line of lines) {
3312
+ const filesMatch = line.match(/(\d+)\s+files?\s+changed/);
3313
+ const insertMatch = line.match(/(\d+)\s+insertions?\(\+\)/);
3314
+ const deleteMatch = line.match(/(\d+)\s+deletions?\(-\)/);
3315
+ if (filesMatch) filesChanged = parseInt(filesMatch[1], 10);
3316
+ if (insertMatch) insertions = parseInt(insertMatch[1], 10);
3317
+ if (deleteMatch) deletions = parseInt(deleteMatch[1], 10);
3318
+ }
3319
+
3320
+ entries.push({
3321
+ hash,
3322
+ date: date || '',
3323
+ message: message || '',
3324
+ refs,
3325
+ body: '',
3326
+ author_name: author_name || '',
3327
+ author_email: author_email || '',
3328
+ filesChanged,
3329
+ insertions,
3330
+ deletions,
3331
+ parents,
3332
+ });
3333
+ }
3334
+
3335
+ return { all: entries, latest: entries[0] || null, total: entries.length };
3336
+ }
3337
+
3021
3338
  const filePath = options.file
3022
3339
  ? (await resolveGitFileContext(directoryPath, directoryGit, options.file, repoRoot)).repoPath
3023
3340
  : undefined;
@@ -3045,7 +3362,7 @@ export async function getLog(directory, options = {}) {
3045
3362
  'log',
3046
3363
  `--max-count=${maxCount}`,
3047
3364
  '--date=iso',
3048
- '--pretty=format:%H%x1f%an%x1f%ae%x1f%ad%x1f%s%x1e',
3365
+ '--pretty=format:%x1e%H%x1f%P%x1f%an%x1f%ae%x1f%ad%x1f%s',
3049
3366
  '--shortstat'
3050
3367
  ];
3051
3368
 
@@ -3072,7 +3389,8 @@ export async function getLog(directory, options = {}) {
3072
3389
  records.forEach((record) => {
3073
3390
  const lines = record.split('\n').filter((line) => line.trim().length > 0);
3074
3391
  const header = lines.shift() || '';
3075
- const [hash] = header.split('\x1f');
3392
+ const [hash, parentsRaw] = header.split('\x1f');
3393
+ const parents = parentsRaw ? parentsRaw.trim().split(' ').filter(Boolean) : [];
3076
3394
  if (!hash) {
3077
3395
  return;
3078
3396
  }
@@ -3097,11 +3415,11 @@ export async function getLog(directory, options = {}) {
3097
3415
  }
3098
3416
  });
3099
3417
 
3100
- statsMap.set(hash, { filesChanged, insertions, deletions });
3418
+ statsMap.set(hash, { filesChanged, insertions, deletions, parents });
3101
3419
  });
3102
3420
 
3103
3421
  const merged = baseLog.all.map((entry) => {
3104
- const stats = statsMap.get(entry.hash) || { filesChanged: 0, insertions: 0, deletions: 0 };
3422
+ const stats = statsMap.get(entry.hash) || { filesChanged: 0, insertions: 0, deletions: 0, parents: [] };
3105
3423
  return {
3106
3424
  hash: entry.hash,
3107
3425
  date: entry.date,
@@ -3112,7 +3430,8 @@ export async function getLog(directory, options = {}) {
3112
3430
  author_email: entry.author_email,
3113
3431
  filesChanged: stats.filesChanged,
3114
3432
  insertions: stats.insertions,
3115
- deletions: stats.deletions
3433
+ deletions: stats.deletions,
3434
+ parents: stats.parents || [],
3116
3435
  };
3117
3436
  });
3118
3437