@openchamber/web 1.11.5 → 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.
- package/README.md +6 -0
- package/bin/cli.js +443 -2
- package/dist/assets/{MarkdownRendererImpl-C3-ZpwEx.js → MarkdownRendererImpl-DaF15QNC.js} +1 -1
- package/dist/assets/{MultiRunWindow-BDfPzMDy.js → MultiRunWindow-Cl7wS_CB.js} +1 -1
- package/dist/assets/{OnboardingScreen-DGgh4IXB.js → OnboardingScreen-DTv6YJI1.js} +2 -2
- package/dist/assets/{SettingsWindow-B8QKr5dB.js → SettingsWindow-_c3TTL2z.js} +1 -1
- package/dist/assets/{TerminalView-D7IIkSGJ.js → TerminalView-CuXkDROt.js} +4 -4
- package/dist/assets/es-CYoUf2D-.js +15 -0
- package/dist/assets/{index-DHluop4D.js → index-3WXrN3AX.js} +1 -1
- package/dist/assets/index-BREIbhcb.css +1 -0
- package/dist/assets/ko-2tM0fIna.js +15 -0
- package/dist/assets/main-BF3kWAJ9.js +239 -0
- package/dist/assets/{main-VVcyjpiF.js → main-o8ZERrmU.js} +2 -2
- package/dist/assets/miniChat-BZQjpK23.js +2 -0
- package/dist/assets/{modelPrefsAutoSave-Ctdc3cCY.js → modelPrefsAutoSave-wwnbqBk7.js} +109 -107
- package/dist/assets/pl-Dq8uAotM.js +15 -0
- package/dist/assets/pt-BR-nh9s9DFT.js +15 -0
- package/dist/assets/{renderElectronMiniChatApp-CsddCM3q.js → renderElectronMiniChatApp-C-Ezew9P.js} +2 -2
- package/dist/assets/uk-BZtz0wUV.js +15 -0
- package/dist/assets/{vendor-.bun-Bum-iBXX.js → vendor-.bun-CV3tusA8.js} +1 -1
- package/dist/assets/zh-CN-j_nYMchE.js +15 -0
- package/dist/assets/zh-TW-B11UpkDJ.js +15 -0
- package/dist/index.html +11 -28
- package/dist/mini-chat.html +4 -4
- package/package.json +1 -1
- package/server/index.js +2 -0
- package/server/lib/cloudflare-tunnel.js +3 -5
- package/server/lib/fs/routes.js +5 -0
- package/server/lib/fs/routes.test.js +61 -1
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +82 -1
- package/server/lib/git/service.js +338 -19
- package/server/lib/git/service.test.js +414 -8
- package/server/lib/ngrok-tunnel.js +209 -0
- package/server/lib/opencode/core-routes.js +1 -0
- package/server/lib/opencode/env-runtime.js +52 -4
- package/server/lib/opencode/env-runtime.test.js +82 -6
- package/server/lib/opencode/feature-routes-runtime.js +35 -0
- package/server/lib/opencode/index.js +19 -0
- package/server/lib/opencode/npm-registry.js +157 -0
- package/server/lib/opencode/npm-registry.test.js +179 -0
- package/server/lib/opencode/openchamber-routes.js +9 -7
- package/server/lib/opencode/plugin-routes.js +373 -0
- package/server/lib/opencode/plugin-routes.test.js +384 -0
- package/server/lib/opencode/plugin-spec.js +107 -0
- package/server/lib/opencode/plugin-spec.test.js +154 -0
- package/server/lib/opencode/plugins.js +393 -0
- package/server/lib/opencode/plugins.test.js +176 -0
- package/server/lib/opencode/settings-helpers.js +6 -0
- package/server/lib/opencode/settings-helpers.test.js +11 -0
- package/server/lib/opencode/settings-runtime.js +39 -1
- package/server/lib/opencode/settings-runtime.test.js +39 -0
- package/server/lib/skills-catalog/source.js +1 -1
- package/server/lib/tunnels/DOCUMENTATION.md +1 -0
- package/server/lib/tunnels/providers/ngrok.js +117 -0
- package/server/lib/tunnels/types.js +2 -0
- package/dist/assets/es-dIVpApmS.js +0 -15
- package/dist/assets/index-Bk9IWJe1.css +0 -1
- package/dist/assets/ko-Cqf3E9-d.js +0 -15
- package/dist/assets/main-D45l3Dxw.js +0 -232
- package/dist/assets/miniChat-a9w7WM0c.js +0 -2
- package/dist/assets/pl-C577DpsX.js +0 -15
- package/dist/assets/pt-BR-BeeF6VlK.js +0 -15
- package/dist/assets/uk-CZ7XVz_D.js +0 -15
- package/dist/assets/zh-CN-BMSSqdyO.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
|
-
|
|
137
|
-
|
|
164
|
+
const normalized = normalizeGitExecutableCandidate(candidate);
|
|
165
|
+
if (isExecutableFile(normalized)) {
|
|
166
|
+
resolvedGitBinary = normalized;
|
|
138
167
|
return resolvedGitBinary;
|
|
139
168
|
}
|
|
140
169
|
}
|
|
141
170
|
|
|
142
|
-
const
|
|
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
|
|
319
|
+
return createSimpleGit({ env, spawnOptions, binary, unsafe });
|
|
280
320
|
}
|
|
281
|
-
return
|
|
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
|
-
|
|
1989
|
-
|
|
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
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
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
|
|
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
|
|