dsh-easygit-plugin 0.2.3 → 0.3.0
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 +66 -67
- package/README.zh-CN.md +66 -67
- package/assets/preview.png +0 -0
- package/lib/client.js +1501 -860
- package/lib/index.js +644 -44
- package/lib/types/client/commit-actions.d.ts +18 -0
- package/lib/types/client/conflict-model.d.ts +15 -0
- package/lib/types/client/conflict-tab.d.ts +14 -0
- package/lib/types/client/index.d.ts +30 -4
- package/lib/types/client/merge-tab.d.ts +16 -0
- package/lib/types/client/panel-controller.d.ts +9 -37
- package/lib/types/client/stash-tab.d.ts +15 -0
- package/lib/types/client/view-model.d.ts +1 -35
- package/lib/types/host/actions.d.ts +6 -3
- package/lib/types/host/conflict-worker.d.ts +1 -0
- package/lib/types/host/git-repository-service.d.ts +13 -2
- package/lib/types/host/stash-worker.d.ts +1 -0
- package/lib/types/shared/contracts.d.ts +197 -0
- package/package.json +3 -4
package/lib/index.js
CHANGED
|
@@ -424,6 +424,14 @@ async function dispatchRepositoryAction(action, sessionId, body, context, depend
|
|
|
424
424
|
if (!context) return { ok: false, code: "SESSION_NOT_FOUND", message: "\u65E0\u6CD5\u786E\u5B9A\u5F53\u524D\u4F1A\u8BDD\u7684\u4ED3\u5E93\u76EE\u5F55" };
|
|
425
425
|
const repository = dependencies.repository;
|
|
426
426
|
const base = { sessionId, workdir: context.workdir, operationId: body.operationId, sandboxPolicy: context.policy };
|
|
427
|
+
if (action === "get-commit-edit-state") return repository.conflictAction(action, context.workdir, body, void 0, context.policy);
|
|
428
|
+
if (action === "amend-message" || action === "amend-commit" || action === "undo-commit" || action === "revert-commit") {
|
|
429
|
+
return repository.conflictAction(action, context.workdir, body, base);
|
|
430
|
+
}
|
|
431
|
+
if (action === "get-conflicts" || action === "get-conflict" || action === "get-merge-preview") return repository.conflictAction(action, context.workdir, body, void 0, context.policy);
|
|
432
|
+
if (action === "save-conflict" || action === "resolve-conflict" || action === "start-operation" || action === "finish-operation" || action === "merge-branch") {
|
|
433
|
+
return repository.conflictAction(action, context.workdir, body, base);
|
|
434
|
+
}
|
|
427
435
|
if (action === "get-summary") return repository.getSummary(context.workdir, void 0, context.policy);
|
|
428
436
|
if (action === "get-diff") return repository.getDiff(context.workdir, body.path, body.staged === true, void 0, context.policy);
|
|
429
437
|
if (action === "get-branches") return repository.getBranches(context.workdir, void 0, context.policy);
|
|
@@ -431,6 +439,12 @@ async function dispatchRepositoryAction(action, sessionId, body, context, depend
|
|
|
431
439
|
if (action === "get-commit-detail") return repository.getCommitDetail(context.workdir, body.hash, void 0, context.policy);
|
|
432
440
|
if (action === "get-commit-diff") return repository.getCommitDiff(context.workdir, body.hash, void 0, context.policy);
|
|
433
441
|
if (action === "get-stashes") return repository.getStashes(context.workdir, void 0, context.policy);
|
|
442
|
+
if (action === "get-stash-detail") return repository.getStashDetail(context.workdir, body.selector, body.hash, void 0, context.policy);
|
|
443
|
+
if (action === "get-stash-diff") return repository.getStashDiff(context.workdir, body.selector, body.hash, body.path, body.untracked === true, void 0, context.policy);
|
|
444
|
+
if (action === "create-stash") return repository.createStash(base, body.message, body.paths, body.includeUntracked === true);
|
|
445
|
+
if (action === "apply-stash" || action === "pop-stash" || action === "drop-stash" || action === "branch-stash") {
|
|
446
|
+
return repository.mutateStash(base, action, body.selector, body.hash, body.name, body.confirmRisk === true);
|
|
447
|
+
}
|
|
434
448
|
if (action === "get-sync-state") return repository.getSyncState(context.workdir, void 0, context.policy);
|
|
435
449
|
if (action === "stage-paths") return repository.stagePaths(base, body.paths);
|
|
436
450
|
if (action === "unstage-paths") return repository.unstagePaths(base, body.paths);
|
|
@@ -517,7 +531,7 @@ async function dispatchProposalAction(action, sessionId, body, dependencies) {
|
|
|
517
531
|
const result = await dependencies.executeProposal(
|
|
518
532
|
dependencies.shell,
|
|
519
533
|
proposal,
|
|
520
|
-
dependencies.resolveExecutionPolicy(sessionId),
|
|
534
|
+
await dependencies.resolveExecutionPolicy(sessionId),
|
|
521
535
|
() => dependencies.flushProposal(sessionId)
|
|
522
536
|
);
|
|
523
537
|
console.log("easygit HTTP execute", proposal.proposalId, "ok=", result.ok);
|
|
@@ -525,12 +539,21 @@ async function dispatchProposalAction(action, sessionId, body, dependencies) {
|
|
|
525
539
|
}
|
|
526
540
|
return { status: 400, data: { ok: false, error: "unknown action: " + action } };
|
|
527
541
|
}
|
|
528
|
-
function registerEasyGitActions(webServer, dependencies) {
|
|
542
|
+
function registerEasyGitActions(webServer, dependencies, connection) {
|
|
529
543
|
if (!webServer) return void 0;
|
|
530
544
|
return webServer.register({
|
|
531
545
|
kind: "prefix",
|
|
532
546
|
path: "/easygit",
|
|
533
547
|
handler: async (req, res) => {
|
|
548
|
+
if (!connection) {
|
|
549
|
+
sendJson(res, 503, { ok: false, error: "connection service unavailable" });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const rejection = connection.requestRejection(req);
|
|
553
|
+
if (rejection !== void 0) {
|
|
554
|
+
sendJson(res, rejection, { ok: false, error: rejection === 401 ? "unauthorized" : "forbidden" });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
534
557
|
if (req.method !== "POST") {
|
|
535
558
|
sendJson(res, 405, { ok: false, error: "method not allowed" });
|
|
536
559
|
return;
|
|
@@ -565,7 +588,7 @@ function registerEasyGitActions(webServer, dependencies) {
|
|
|
565
588
|
try {
|
|
566
589
|
await dependencies.proposalStorageReady;
|
|
567
590
|
if (isRepositoryAction(action)) {
|
|
568
|
-
const context = dependencies.repositoryContext(sessionId);
|
|
591
|
+
const context = await dependencies.repositoryContext(sessionId);
|
|
569
592
|
const result = await dispatchRepositoryAction(action, sessionId, body, context, dependencies);
|
|
570
593
|
sendJson(res, 200, await attachRepositoryRecovery(action, sessionId, body, result, context, dependencies));
|
|
571
594
|
return;
|
|
@@ -585,6 +608,19 @@ var init_actions = __esm({
|
|
|
585
608
|
"use strict";
|
|
586
609
|
init_command_policy();
|
|
587
610
|
REPOSITORY_ACTIONS = [
|
|
611
|
+
"get-commit-edit-state",
|
|
612
|
+
"amend-message",
|
|
613
|
+
"amend-commit",
|
|
614
|
+
"undo-commit",
|
|
615
|
+
"revert-commit",
|
|
616
|
+
"get-merge-preview",
|
|
617
|
+
"merge-branch",
|
|
618
|
+
"get-conflicts",
|
|
619
|
+
"get-conflict",
|
|
620
|
+
"save-conflict",
|
|
621
|
+
"resolve-conflict",
|
|
622
|
+
"start-operation",
|
|
623
|
+
"finish-operation",
|
|
588
624
|
"get-summary",
|
|
589
625
|
"get-diff",
|
|
590
626
|
"get-branches",
|
|
@@ -592,6 +628,13 @@ var init_actions = __esm({
|
|
|
592
628
|
"get-commit-detail",
|
|
593
629
|
"get-commit-diff",
|
|
594
630
|
"get-stashes",
|
|
631
|
+
"get-stash-detail",
|
|
632
|
+
"get-stash-diff",
|
|
633
|
+
"create-stash",
|
|
634
|
+
"apply-stash",
|
|
635
|
+
"pop-stash",
|
|
636
|
+
"drop-stash",
|
|
637
|
+
"branch-stash",
|
|
595
638
|
"get-sync-state",
|
|
596
639
|
"stage-paths",
|
|
597
640
|
"unstage-paths",
|
|
@@ -772,7 +815,416 @@ var init_proposal_service = __esm({
|
|
|
772
815
|
}
|
|
773
816
|
});
|
|
774
817
|
|
|
818
|
+
// src/host/conflict-worker.ts
|
|
819
|
+
function conflictWorkerCommand(action, payload) {
|
|
820
|
+
const data = { ...payload };
|
|
821
|
+
if (typeof data.content === "string") {
|
|
822
|
+
data.contentBase64 = Buffer.from(data.content).toString("base64");
|
|
823
|
+
delete data.content;
|
|
824
|
+
}
|
|
825
|
+
return quoteShellArg(process.execPath) + " -e " + quoteShellArg(worker) + " " + quoteShellArg(Buffer.from(JSON.stringify({ ...data, action })).toString("base64"));
|
|
826
|
+
}
|
|
827
|
+
var worker;
|
|
828
|
+
var init_conflict_worker = __esm({
|
|
829
|
+
"src/host/conflict-worker.ts"() {
|
|
830
|
+
"use strict";
|
|
831
|
+
init_command_policy();
|
|
832
|
+
worker = String.raw`
|
|
833
|
+
const fs = require('node:fs');
|
|
834
|
+
const path = require('node:path');
|
|
835
|
+
const crypto = require('node:crypto');
|
|
836
|
+
const { execFileSync } = require('node:child_process');
|
|
837
|
+
const input = JSON.parse(Buffer.from(process.argv[1], 'base64').toString('utf8'));
|
|
838
|
+
const LIMIT = 48 * 1024;
|
|
839
|
+
const fail = (message, code = 'STATE_CONFLICT') => { throw Object.assign(new Error(message), { code }); };
|
|
840
|
+
const git = (...args) => execFileSync('git', args, { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_EDITOR: 'true', GIT_SEQUENCE_EDITOR: 'true' } });
|
|
841
|
+
const hash = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
842
|
+
const exists = file => fs.existsSync(file);
|
|
843
|
+
const gitPath = name => git('rev-parse', '--git-path', name).trim();
|
|
844
|
+
function squashState() {
|
|
845
|
+
const file = gitPath('easygit-squash.json');
|
|
846
|
+
if (!exists(file) || !exists(gitPath('SQUASH_MSG'))) return null;
|
|
847
|
+
const saved = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
848
|
+
// A commit or branch switch outside the workbench ends our ownership.
|
|
849
|
+
if (saved.head !== git('rev-parse', 'HEAD').trim() || saved.branch !== 'refs/heads/' + git('branch', '--show-current').trim()) return null;
|
|
850
|
+
return saved;
|
|
851
|
+
}
|
|
852
|
+
function operationState() {
|
|
853
|
+
const dir = name => git('rev-parse', '--git-path', name).trim();
|
|
854
|
+
const names = ['rebase-merge', 'rebase-apply', 'MERGE_HEAD', 'CHERRY_PICK_HEAD', 'sequencer', 'REVERT_HEAD'];
|
|
855
|
+
const paths = names.map(dir);
|
|
856
|
+
const squash = squashState();
|
|
857
|
+
const todo = exists(path.join(paths[4], 'todo')) ? fs.readFileSync(path.join(paths[4], 'todo'), 'utf8') : '';
|
|
858
|
+
const operation = exists(paths[0]) || exists(paths[1]) ? 'rebase' : exists(paths[2]) ? 'merge' : exists(paths[3]) || /^pick /m.test(todo) ? 'cherry-pick' : exists(paths[5]) || /^revert /m.test(todo) ? 'revert' : squash ? 'merge' : null;
|
|
859
|
+
const meta = ['HEAD', 'MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD', 'rebase-merge/head-name', 'rebase-merge/onto', 'rebase-merge/msgnum', 'rebase-merge/stopped-sha', 'rebase-apply/next', 'rebase-apply/orig-head', 'sequencer/todo'].map(name => {
|
|
860
|
+
const file = dir(name); return exists(file) ? fs.readFileSync(file).toString('base64') : '';
|
|
861
|
+
});
|
|
862
|
+
let head;
|
|
863
|
+
try { head = git('rev-parse', '--verify', 'HEAD').trim(); }
|
|
864
|
+
catch (error) {
|
|
865
|
+
const branch = git('symbolic-ref', '--quiet', 'HEAD').trim();
|
|
866
|
+
if (exists(dir(branch))) throw error;
|
|
867
|
+
head = 'unborn:' + branch;
|
|
868
|
+
}
|
|
869
|
+
return { operation, ...(squash && operation === 'merge' ? { mergeMode: 'squash' } : {}), operationToken: hash(JSON.stringify([operation, meta, head, squash])) };
|
|
870
|
+
}
|
|
871
|
+
function mergeSnapshot() {
|
|
872
|
+
if (typeof input.target !== 'string' || !/^refs\/(heads|remotes)\/.+/.test(input.target) || /[\0\r\n]/.test(input.target)) fail('请选择本地或远程源分支', 'INVALID_ARGUMENT');
|
|
873
|
+
git('check-ref-format', input.target);
|
|
874
|
+
const name = git('branch', '--show-current').trim();
|
|
875
|
+
if (!name) fail('分离 HEAD 状态不能合并分支,请先切换到本地分支');
|
|
876
|
+
const branch = 'refs/heads/' + name;
|
|
877
|
+
if (branch === input.target) fail('不能将当前分支合并到自身');
|
|
878
|
+
const head = git('rev-parse', '--verify', 'HEAD').trim();
|
|
879
|
+
const sourceHead = git('rev-parse', '--verify', '--end-of-options', input.target + '^{commit}').trim();
|
|
880
|
+
return { branch: branch.slice('refs/heads/'.length), target: input.target, head, sourceHead, token: hash(JSON.stringify([branch, input.target, head, sourceHead])) };
|
|
881
|
+
}
|
|
882
|
+
function ancestor(a, b) {
|
|
883
|
+
try { git('merge-base', '--is-ancestor', a, b); return true; }
|
|
884
|
+
catch (error) { if (error.status === 1) return false; throw error; }
|
|
885
|
+
}
|
|
886
|
+
function previewMerge() {
|
|
887
|
+
const snapshot = mergeSnapshot();
|
|
888
|
+
const { head, sourceHead } = snapshot;
|
|
889
|
+
let base;
|
|
890
|
+
try { base = git('merge-base', head, sourceHead).trim(); }
|
|
891
|
+
catch (error) { if (error.status === 1) fail('两个分支没有共同祖先,不支持合并无关历史'); throw error; }
|
|
892
|
+
const fields = git('log', '--no-color', '--max-count=201', '--format=%H%x00%s%x00%an%x00%aI%x00', head + '..' + sourceHead).split('\0');
|
|
893
|
+
const commits = [];
|
|
894
|
+
for (let i = 0; i + 3 < fields.length; i += 4) commits.push({ hash: fields[i].trim(), subject: fields[i + 1], author: fields[i + 2], date: fields[i + 3] });
|
|
895
|
+
const files = git('diff', '--name-only', '-z', base, sourceHead, '--').split('\0').filter(Boolean);
|
|
896
|
+
let diff, overflow = false;
|
|
897
|
+
try { diff = git('-c', 'core.quotePath=false', 'diff', '--no-ext-diff', '--no-textconv', '--no-color', base, sourceHead, '--'); }
|
|
898
|
+
catch (error) { if (error.code !== 'ENOBUFS') throw error; diff = String(error.stdout || ''); overflow = true; }
|
|
899
|
+
return { ...snapshot, base, canFastForward: ancestor(head, sourceHead), alreadyMerged: ancestor(sourceHead, head), commits: commits.slice(0, 200), commitsTruncated: commits.length > 200, files: files.slice(0, 500), filesTruncated: files.length > 500, diff: diff.slice(0, 180000), diffTruncated: overflow || diff.length > 180000 };
|
|
900
|
+
}
|
|
901
|
+
function entries() {
|
|
902
|
+
return git('ls-files', '--unmerged', '-z').split('\0').filter(Boolean).map(row => {
|
|
903
|
+
const match = /^(\d+) ([0-9a-f]+) ([123])\t([\s\S]*)$/.exec(row);
|
|
904
|
+
if (!match) fail('无法读取完整冲突索引');
|
|
905
|
+
return { mode: match[1], oid: match[2], stage: Number(match[3]), path: match[4] };
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
function state() {
|
|
909
|
+
const grouped = new Map();
|
|
910
|
+
for (const entry of entries()) {
|
|
911
|
+
if (!grouped.has(entry.path)) grouped.set(entry.path, []);
|
|
912
|
+
grouped.get(entry.path).push(entry.stage);
|
|
913
|
+
}
|
|
914
|
+
return { ...operationState(), files: [...grouped].map(([file, stages]) => ({ path: file, stages, kind: !stages.includes(1) ? '双方新增' : !stages.includes(2) || !stages.includes(3) ? '删除 / 修改' : '双方修改' })) };
|
|
915
|
+
}
|
|
916
|
+
function safePath(file) {
|
|
917
|
+
if (typeof file !== 'string' || !file || file.includes('\0') || path.isAbsolute(file) || file.split(/[\\/]/).some(part => part === '..' || part.toLowerCase() === '.git')) fail('无效的仓库相对路径', 'INVALID_ARGUMENT');
|
|
918
|
+
let current = process.cwd();
|
|
919
|
+
const parts = file.split('/');
|
|
920
|
+
for (let i = 0; i < parts.length; i++) {
|
|
921
|
+
current = path.join(current, parts[i]);
|
|
922
|
+
try {
|
|
923
|
+
const stat = fs.lstatSync(current);
|
|
924
|
+
if (stat.isSymbolicLink()) fail('符号链接冲突请在外部工具中处理');
|
|
925
|
+
if (i < parts.length - 1 && !stat.isDirectory()) fail('路径包含非目录节点');
|
|
926
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
927
|
+
}
|
|
928
|
+
return current;
|
|
929
|
+
}
|
|
930
|
+
function version(buffer, mode = null) {
|
|
931
|
+
if (buffer === null) return { exists: false, text: null, mode, reason: null };
|
|
932
|
+
const text = buffer.toString('utf8');
|
|
933
|
+
const reason = !['100644', '100755', null].includes(mode) ? '符号链接或子模块,请使用外部工具处理' : buffer.length > LIMIT ? '文件超过 48 KiB,请使用外部工具处理' : buffer.includes(0) || !Buffer.from(text).equals(buffer) ? '二进制或非 UTF-8 文件,可选择整份一方版本' : null;
|
|
934
|
+
return { exists: true, text: reason ? null : text, mode, reason };
|
|
935
|
+
}
|
|
936
|
+
function detail(file) {
|
|
937
|
+
const rows = entries().filter(row => row.path === file);
|
|
938
|
+
if (!rows.length) fail('该文件已不在冲突列表中,请刷新');
|
|
939
|
+
const full = safePath(file);
|
|
940
|
+
let working = null;
|
|
941
|
+
try {
|
|
942
|
+
const stat = fs.lstatSync(full);
|
|
943
|
+
if (!stat.isFile()) fail('目录、符号链接或子模块冲突请在外部工具中处理');
|
|
944
|
+
if (stat.size > 8 * 1024 * 1024) fail('文件过大,请使用外部工具处理');
|
|
945
|
+
working = fs.readFileSync(full);
|
|
946
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
947
|
+
const versions = [1, 2, 3].map(stage => {
|
|
948
|
+
const row = rows.find(row => row.stage === stage);
|
|
949
|
+
if (!row) return version(null);
|
|
950
|
+
if (!['100644', '100755'].includes(row.mode)) return { exists: true, text: null, mode: row.mode, reason: '符号链接或子模块,请使用外部工具处理' };
|
|
951
|
+
const size = Number(git('cat-file', '-s', row.oid).trim());
|
|
952
|
+
if (size > LIMIT) return { exists: true, text: null, mode: row.mode, reason: '文件超过 48 KiB,请使用外部工具处理' };
|
|
953
|
+
return version(execFileSync('git', ['cat-file', 'blob', row.oid], { maxBuffer: LIMIT + 1024 }), row.mode);
|
|
954
|
+
});
|
|
955
|
+
const op = operationState();
|
|
956
|
+
const squash = op.mergeMode === 'squash' ? squashState() : null;
|
|
957
|
+
for (const [index, ref] of [[1, 'HEAD'], [2, op.operation === 'merge' ? 'MERGE_HEAD' : op.operation === 'rebase' ? 'REBASE_HEAD' : op.operation === 'cherry-pick' ? 'CHERRY_PICK_HEAD' : null]]) {
|
|
958
|
+
const row = rows.find(row => row.stage === index + 1);
|
|
959
|
+
versions[index].source = row ? '索引 stage ' + (index + 1) + ' · blob ' + row.oid.slice(0, 12) : '该方无文件';
|
|
960
|
+
if (ref && op.operation) {
|
|
961
|
+
try { versions[index].source = ref + ' · ' + git('rev-parse', '--verify', ref + '^{commit}').trim().slice(0, 12); } catch {}
|
|
962
|
+
}
|
|
963
|
+
if (index === 2 && squash) versions[index].source = '压缩合并源 · ' + squash.sourceHead.slice(0, 12);
|
|
964
|
+
}
|
|
965
|
+
const result = version(working);
|
|
966
|
+
const attr = git('check-attr', '-z', 'conflict-marker-size', '--', file).split('\0')[2];
|
|
967
|
+
const markerSize = /^\d+$/.test(attr || '') ? Number(attr) : 7;
|
|
968
|
+
if (markerSize < 1 || markerSize > 1024) fail('冲突标记长度不受支持,请使用外部工具处理');
|
|
969
|
+
return { path: file, operation: op.operation, token: hash(JSON.stringify([rows, op.operationToken, working === null ? null : working.toString('base64')])), base: versions[0], ours: versions[1], theirs: versions[2], result, editable: versions.every(v => !v.reason) && !result.reason, special: !versions[1].exists || !versions[2].exists || versions.some(v => !!v.reason) || !!result.reason, markerSize };
|
|
970
|
+
}
|
|
971
|
+
function markers(text, size) {
|
|
972
|
+
return text.split(/\r?\n/).some(line => ['<', '|', '=', '>'].some(c =>
|
|
973
|
+
line.startsWith(c.repeat(size)) && (line.length === size || /^[ \t]/.test(line.slice(size)))
|
|
974
|
+
));
|
|
975
|
+
}
|
|
976
|
+
function commitEditState() {
|
|
977
|
+
const head = git('rev-parse', '--verify', 'HEAD').trim();
|
|
978
|
+
const branch = git('branch', '--show-current').trim();
|
|
979
|
+
const op = operationState();
|
|
980
|
+
const index = git('ls-files', '--stage', '-z');
|
|
981
|
+
return {
|
|
982
|
+
head, branch, message: git('show', '-s', '--format=%B', head).replace(/\n$/, ''),
|
|
983
|
+
parents: git('show', '-s', '--format=%P', head).trim().split(' ').filter(Boolean),
|
|
984
|
+
staged: !!git('diff', '--cached', '--name-only', '-z'),
|
|
985
|
+
dirty: !!git('status', '--porcelain', '--untracked-files=all'),
|
|
986
|
+
blocked: !!op.operation || !!entries().length,
|
|
987
|
+
token: hash(JSON.stringify([head, branch, index, op.operationToken])),
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
function editCommit() {
|
|
991
|
+
if (input.confirmRisk !== true) fail('请先确认提交操作的影响', 'PERMISSION_DENIED');
|
|
992
|
+
const snapshot = commitEditState();
|
|
993
|
+
if (snapshot.blocked) fail('请先完成或中止当前 Git 操作并解决冲突');
|
|
994
|
+
if (!snapshot.branch) fail('请先切换到本地分支');
|
|
995
|
+
if (input.token !== snapshot.token) fail('分支、最近提交或暂存区已变化,请刷新后重新确认');
|
|
996
|
+
if (input.action === 'amend-message') {
|
|
997
|
+
if (typeof input.message !== 'string' || !input.message.trim() || Buffer.byteLength(input.message) > LIMIT || /[\0\r]/.test(input.message)) fail('提交说明必须是 48 KiB 以内的非空文本,不得包含 NUL 或回车', 'INVALID_ARGUMENT');
|
|
998
|
+
// --only without paths amends the message while preserving the real index.
|
|
999
|
+
git('commit', '--amend', '--only', '--allow-empty', '--cleanup=verbatim', '-m', input.message);
|
|
1000
|
+
} else if (input.action === 'amend-commit') {
|
|
1001
|
+
if (!snapshot.staged) fail('没有已暂存的改动,请先暂存需要补充的文件');
|
|
1002
|
+
git('commit', '--amend', '--no-edit', '--cleanup=verbatim');
|
|
1003
|
+
} else if (input.action === 'undo-commit') {
|
|
1004
|
+
if (!snapshot.parents.length) fail('根提交没有父提交,无法执行保留暂存区的撤销');
|
|
1005
|
+
git('reset', '--soft', snapshot.parents[0]);
|
|
1006
|
+
} else {
|
|
1007
|
+
if (snapshot.dirty) fail('Revert 前请先提交或贮藏工作区改动');
|
|
1008
|
+
if (typeof input.hash !== 'string' || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(input.hash)) fail('请选择完整的提交 SHA', 'INVALID_ARGUMENT');
|
|
1009
|
+
if (!ancestor(input.hash, snapshot.head)) fail('目标提交不在当前分支历史中');
|
|
1010
|
+
const parents = git('show', '-s', '--format=%P', input.hash).trim().split(' ').filter(Boolean);
|
|
1011
|
+
if (parents.length > 1) {
|
|
1012
|
+
if (!Number.isInteger(input.mainline) || input.mainline < 1 || input.mainline > parents.length) fail('Revert 合并提交前必须选择主线父提交', 'INVALID_ARGUMENT');
|
|
1013
|
+
} else if (input.mainline !== undefined) fail('普通提交无需选择主线父提交', 'INVALID_ARGUMENT');
|
|
1014
|
+
git('revert', '--no-edit', ...(parents.length > 1 ? ['-m', String(input.mainline)] : []), input.hash);
|
|
1015
|
+
}
|
|
1016
|
+
return state();
|
|
1017
|
+
}
|
|
1018
|
+
function main() {
|
|
1019
|
+
process.chdir(git('rev-parse', '--show-toplevel').trim());
|
|
1020
|
+
if (input.action === 'get-commit-edit-state') return commitEditState();
|
|
1021
|
+
if (['amend-message', 'amend-commit', 'undo-commit', 'revert-commit'].includes(input.action)) return editCommit();
|
|
1022
|
+
if (input.action === 'get-merge-preview') return previewMerge();
|
|
1023
|
+
if (input.action === 'merge-branch') {
|
|
1024
|
+
if (!['normal', 'ff-only', 'squash'].includes(input.mode)) fail('无效的合并方式', 'INVALID_ARGUMENT');
|
|
1025
|
+
if (state().operation || entries().length) fail('请先完成或中止当前 Git 操作');
|
|
1026
|
+
if (git('status', '--porcelain', '--untracked-files=all').trim()) fail('合并前请先提交或贮藏工作区改动');
|
|
1027
|
+
const snapshot = mergeSnapshot();
|
|
1028
|
+
if (snapshot.token !== input.token) fail('分支已变化,请重新预览后合并');
|
|
1029
|
+
if (ancestor(snapshot.sourceHead, snapshot.head)) return state();
|
|
1030
|
+
if (input.mode === 'squash') {
|
|
1031
|
+
fs.writeFileSync(gitPath('easygit-squash.json'), JSON.stringify({ head: snapshot.head, branch: 'refs/heads/' + snapshot.branch, sourceHead: snapshot.sourceHead }));
|
|
1032
|
+
}
|
|
1033
|
+
// Explicit flags keep user merge.ff / branch mergeOptions from changing the selected mode.
|
|
1034
|
+
try {
|
|
1035
|
+
git('-c', 'merge.autoStash=false', 'merge', '--no-edit', '--no-autostash', '--no-overwrite-ignore', ...(input.mode === 'squash' ? ['--squash', '--ff', '--no-commit'] : input.mode === 'ff-only' ? ['--ff-only', '--no-squash', '--commit'] : ['--ff', '--no-squash', '--commit']), snapshot.sourceHead);
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
if (input.mode === 'squash' && !entries().length && !git('status', '--porcelain').trim()) fs.unlinkSync(gitPath('easygit-squash.json'));
|
|
1038
|
+
throw error;
|
|
1039
|
+
}
|
|
1040
|
+
return state();
|
|
1041
|
+
}
|
|
1042
|
+
if (input.action === 'get-conflicts') return state();
|
|
1043
|
+
if (input.action === 'get-conflict') return detail(input.path);
|
|
1044
|
+
if (input.action === 'save-conflict' || input.action === 'resolve-conflict') {
|
|
1045
|
+
const before = detail(input.path);
|
|
1046
|
+
if (typeof input.token !== 'string' || input.token !== before.token) fail('文件或冲突状态已被外部修改,请刷新后重新处理');
|
|
1047
|
+
const full = safePath(input.path);
|
|
1048
|
+
if (input.action === 'save-conflict') {
|
|
1049
|
+
if (!before.editable) fail('该文件不支持文本编辑');
|
|
1050
|
+
const content = Buffer.from(input.contentBase64, 'base64');
|
|
1051
|
+
if (content.length > LIMIT || content.includes(0)) fail('结果必须是 48 KiB 以内的 UTF-8 文本', 'INVALID_ARGUMENT');
|
|
1052
|
+
const parent = path.dirname(full);
|
|
1053
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
1054
|
+
const mode = exists(full) ? fs.statSync(full).mode : parseInt(before.ours.mode || before.theirs.mode || '100644', 8);
|
|
1055
|
+
const temp = path.join(parent, '.easygit-' + crypto.randomUUID());
|
|
1056
|
+
try {
|
|
1057
|
+
fs.writeFileSync(temp, content, { flag: 'wx', mode });
|
|
1058
|
+
if (detail(input.path).token !== before.token) fail('保存前文件发生变化,请刷新');
|
|
1059
|
+
fs.renameSync(temp, full);
|
|
1060
|
+
} finally { if (exists(temp)) fs.unlinkSync(temp); }
|
|
1061
|
+
return detail(input.path);
|
|
1062
|
+
}
|
|
1063
|
+
if (!['result', 'ours', 'theirs', 'delete'].includes(input.choice)) fail('无效的解决方式', 'INVALID_ARGUMENT');
|
|
1064
|
+
if (['ours', 'theirs'].includes(input.choice)) {
|
|
1065
|
+
const selected = before[input.choice];
|
|
1066
|
+
if (!selected.exists) fail('该方版本不存在,请明确选择删除文件');
|
|
1067
|
+
if (selected.reason && !selected.reason.startsWith('二进制')) fail(selected.reason);
|
|
1068
|
+
if (selected.text !== null && markers(selected.text, before.markerSize)) fail('所选版本仍包含冲突标记,请手动编辑');
|
|
1069
|
+
git('checkout', '--' + input.choice, '--', ':(literal)' + input.path);
|
|
1070
|
+
} else if (input.choice === 'delete') {
|
|
1071
|
+
if (exists(full)) fs.unlinkSync(full);
|
|
1072
|
+
} else {
|
|
1073
|
+
if (before.result.text === null) fail('请明确选择一方、删除文件或在外部工具中处理');
|
|
1074
|
+
if (markers(before.result.text, before.markerSize)) fail('结果中仍有冲突标记,请先逐块解决并保存');
|
|
1075
|
+
}
|
|
1076
|
+
git('add', '--', ':(literal)' + input.path);
|
|
1077
|
+
return state();
|
|
1078
|
+
}
|
|
1079
|
+
if (input.action === 'start-operation') {
|
|
1080
|
+
if (!['merge', 'rebase', 'cherry-pick'].includes(input.kind)) fail('不支持的 Git 操作', 'INVALID_ARGUMENT');
|
|
1081
|
+
if (input.confirmRisk !== true) fail('请先确认 Git 操作风险', 'PERMISSION_DENIED');
|
|
1082
|
+
if (state().operation || entries().length) fail('请先完成或中止当前 Git 操作');
|
|
1083
|
+
if (git('status', '--porcelain').trim()) fail('请先提交或贮藏工作区改动');
|
|
1084
|
+
if (typeof input.target !== 'string' || !input.target || input.target.startsWith('-') || /[\0\r\n]/.test(input.target)) fail('无效的目标引用', 'INVALID_ARGUMENT');
|
|
1085
|
+
const oid = git('rev-parse', '--verify', '--end-of-options', input.target + '^{commit}').trim();
|
|
1086
|
+
git(...(input.kind === 'merge' ? ['merge', '--no-edit', oid] : [input.kind, oid]));
|
|
1087
|
+
return state();
|
|
1088
|
+
}
|
|
1089
|
+
if (input.action === 'finish-operation') {
|
|
1090
|
+
const current = state();
|
|
1091
|
+
if (input.confirmRisk !== true) fail('请先确认继续、中止或跳过的风险', 'PERMISSION_DENIED');
|
|
1092
|
+
if (!current.operation || current.operation !== input.kind || current.operationToken !== input.token) fail('Git 操作状态已改变,请刷新');
|
|
1093
|
+
if (!['continue', 'abort', 'skip'].includes(input.mode) || (input.mode === 'skip' && input.kind === 'merge')) fail('不支持的后续操作', 'INVALID_ARGUMENT');
|
|
1094
|
+
if (input.mode === 'continue' && current.files.length) fail('仍有未标记解决的冲突文件');
|
|
1095
|
+
if (current.mergeMode === 'squash') {
|
|
1096
|
+
const saved = squashState();
|
|
1097
|
+
if (input.mode === 'abort') git('reset', '--merge', saved.head);
|
|
1098
|
+
else git('commit', '--no-edit', '-F', gitPath('SQUASH_MSG'));
|
|
1099
|
+
fs.unlinkSync(gitPath('easygit-squash.json'));
|
|
1100
|
+
if (exists(gitPath('SQUASH_MSG'))) fs.unlinkSync(gitPath('SQUASH_MSG'));
|
|
1101
|
+
} else git(input.kind, '--' + input.mode);
|
|
1102
|
+
return state();
|
|
1103
|
+
}
|
|
1104
|
+
fail('无效操作', 'INVALID_ARGUMENT');
|
|
1105
|
+
}
|
|
1106
|
+
try { console.log(JSON.stringify({ ok: true, data: main() })); }
|
|
1107
|
+
catch (error) {
|
|
1108
|
+
let pending = null;
|
|
1109
|
+
try { pending = state(); } catch {}
|
|
1110
|
+
// A new conflict is an expected stop in a multi-step Git operation.
|
|
1111
|
+
if ((input.action === 'start-operation' || input.action === 'merge-branch' || input.action === 'revert-commit' || (input.action === 'finish-operation' && input.mode !== 'abort')) && error.status && pending && pending.operation === (input.action === 'merge-branch' ? 'merge' : input.action === 'revert-commit' ? 'revert' : input.kind) && (pending.files.length || input.action === 'revert-commit')) {
|
|
1112
|
+
console.log(JSON.stringify({ ok: true, data: pending }));
|
|
1113
|
+
} else console.log(JSON.stringify({ ok: false, code: ['STATE_CONFLICT', 'INVALID_ARGUMENT', 'PERMISSION_DENIED'].includes(error.code) ? error.code : 'GIT_FAILED', message: error.status ? 'Git 操作未完成,请查看详情' : error.message, diagnostics: [error.stdout, error.stderr].filter(Boolean).map(value => value.toString()).join('\n') || undefined }));
|
|
1114
|
+
}
|
|
1115
|
+
`;
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// src/host/stash-worker.ts
|
|
1120
|
+
function createStashCommand(message, paths, includeUntracked) {
|
|
1121
|
+
const payload = Buffer.from(JSON.stringify({ message, paths, includeUntracked })).toString("base64");
|
|
1122
|
+
return "node -e " + quoteShellArg(worker2) + " " + quoteShellArg(payload);
|
|
1123
|
+
}
|
|
1124
|
+
var worker2;
|
|
1125
|
+
var init_stash_worker = __esm({
|
|
1126
|
+
"src/host/stash-worker.ts"() {
|
|
1127
|
+
"use strict";
|
|
1128
|
+
init_command_policy();
|
|
1129
|
+
worker2 = String.raw`
|
|
1130
|
+
const fs = require('node:fs');
|
|
1131
|
+
const os = require('node:os');
|
|
1132
|
+
const path = require('node:path');
|
|
1133
|
+
const { execFileSync } = require('node:child_process');
|
|
1134
|
+
const input = JSON.parse(Buffer.from(process.argv[1], 'base64').toString('utf8'));
|
|
1135
|
+
const env = { ...process.env };
|
|
1136
|
+
const literal = names => names.map(name => ':(literal)' + name);
|
|
1137
|
+
const git = (args, extra = {}) => execFileSync('git', args, { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], env, ...extra });
|
|
1138
|
+
let temporary;
|
|
1139
|
+
try {
|
|
1140
|
+
process.chdir(git(['rev-parse', '--show-toplevel']).trim());
|
|
1141
|
+
const records = git(['status', '--porcelain=v1', '--untracked-files=all', '-z']).split('\0');
|
|
1142
|
+
const changes = [];
|
|
1143
|
+
for (let i = 0; i < records.length - 1; i++) {
|
|
1144
|
+
const row = records[i];
|
|
1145
|
+
const status = row.slice(0, 2);
|
|
1146
|
+
const file = { path: row.slice(3), status };
|
|
1147
|
+
if (/[RC]/.test(status)) file.originalPath = records[++i];
|
|
1148
|
+
if (status.includes('U') || status === 'AA' || status === 'DD') throw new Error('请先解决冲突,再创建贮藏');
|
|
1149
|
+
changes.push(file);
|
|
1150
|
+
}
|
|
1151
|
+
const eligible = changes.filter(file => input.includeUntracked || file.status !== '??');
|
|
1152
|
+
const selected = input.paths ? [...new Set(input.paths)].flatMap(name => {
|
|
1153
|
+
const files = eligible.filter(file => file.path === name);
|
|
1154
|
+
if (!files.length) throw new Error('所选文件已变化或未启用包含未跟踪文件,请刷新:' + name);
|
|
1155
|
+
return files;
|
|
1156
|
+
}) : eligible;
|
|
1157
|
+
if (!selected.length) throw new Error('没有可贮藏的改动');
|
|
1158
|
+
const args = ['stash', 'push'];
|
|
1159
|
+
if (input.includeUntracked) args.push('--include-untracked');
|
|
1160
|
+
if (input.message.trim()) args.push('--message', input.message.trim());
|
|
1161
|
+
if (!input.paths) {
|
|
1162
|
+
git(args);
|
|
1163
|
+
} else {
|
|
1164
|
+
// Native path-limited stash also captures unrelated staged files, and
|
|
1165
|
+
// staged deletions can fail during its cleanup. Build the standard stash
|
|
1166
|
+
// trees in a temporary index, store them, then restore only selected paths.
|
|
1167
|
+
const tracked = selected.filter(file => file.status !== '??');
|
|
1168
|
+
const untracked = selected.filter(file => file.status === '??');
|
|
1169
|
+
const paths = [...new Set(tracked.flatMap(file => file.originalPath && file.status.includes('R') ? [file.path, file.originalPath] : [file.path]))];
|
|
1170
|
+
if (paths.some(name => changes.some(file => file.path === name && file.status === '??') && !untracked.some(file => file.path === name))) {
|
|
1171
|
+
throw new Error('已删除或重命名的路径上存在未跟踪文件,请同时选择该文件并包含未跟踪文件');
|
|
1172
|
+
}
|
|
1173
|
+
temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'easygit-stash-'));
|
|
1174
|
+
const isolated = { ...env, GIT_INDEX_FILE: path.join(temporary, 'index') };
|
|
1175
|
+
const base = git(['rev-parse', 'HEAD']).trim();
|
|
1176
|
+
git(['read-tree', base], { env: isolated });
|
|
1177
|
+
const intentPaths = tracked.filter(file => file.status === ' A').map(file => file.path);
|
|
1178
|
+
if (paths.length) {
|
|
1179
|
+
// Copy object IDs/modes directly; a textual diff can be altered by user
|
|
1180
|
+
// color, prefix or whitespace settings. Missing entries represent deletes.
|
|
1181
|
+
const entries = git(['ls-files', '--stage', '-z', '--', ...literal(paths)]).split('\0')
|
|
1182
|
+
.filter(row => row && !intentPaths.includes(row.slice(row.indexOf('\t') + 1)));
|
|
1183
|
+
const removed = paths.map(name => '0 ' + '0'.repeat(base.length) + '\t' + name);
|
|
1184
|
+
git(['update-index', '-z', '--index-info'], { env: isolated, input: [...removed, ...entries].join('\0') + '\0' });
|
|
1185
|
+
}
|
|
1186
|
+
const indexTree = git(['write-tree'], { env: isolated }).trim();
|
|
1187
|
+
if (intentPaths.length) git(['add', '--', ...literal(intentPaths)], { env: isolated });
|
|
1188
|
+
// -u records tracked worktree edits/deletions without absorbing untracked files.
|
|
1189
|
+
const indexed = git(['ls-files', '-z'], { env: isolated }).split('\0');
|
|
1190
|
+
const workPaths = paths.filter(name => indexed.includes(name));
|
|
1191
|
+
if (workPaths.length) git(['add', '-u', '--', ...literal(workPaths)], { env: isolated });
|
|
1192
|
+
const workTree = git(['write-tree'], { env: isolated }).trim();
|
|
1193
|
+
const indexCommit = git(['commit-tree', indexTree, '-p', base], { input: 'index for selected stash\n' }).trim();
|
|
1194
|
+
const parents = ['-p', base, '-p', indexCommit];
|
|
1195
|
+
if (untracked.length) {
|
|
1196
|
+
git(['read-tree', '--empty'], { env: isolated });
|
|
1197
|
+
git(['add', '--', ...literal(untracked.map(file => file.path))], { env: isolated });
|
|
1198
|
+
const tree = git(['write-tree'], { env: isolated }).trim();
|
|
1199
|
+
parents.push('-p', git(['commit-tree', tree], { input: 'untracked files for selected stash\n' }).trim());
|
|
1200
|
+
}
|
|
1201
|
+
const branch = git(['branch', '--show-current']).trim() || '(detached HEAD)';
|
|
1202
|
+
const message = 'On ' + branch + ': ' + (input.message.trim() || 'selected changes');
|
|
1203
|
+
const stash = git(['commit-tree', workTree, ...parents], { input: message + '\n' }).trim();
|
|
1204
|
+
git(['stash', 'store', '--message', message, stash]);
|
|
1205
|
+
// All selected versions are durable before changing the working files/index.
|
|
1206
|
+
if (paths.length) git(['restore', '--source=' + base, '--staged', '--worktree', '--', ...literal(paths)]);
|
|
1207
|
+
for (const file of untracked) {
|
|
1208
|
+
// A recreated, previously tracked path has just been restored from HEAD.
|
|
1209
|
+
if (!paths.includes(file.path)) fs.unlinkSync(file.path);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
process.stdout.write(JSON.stringify({ ok: true }));
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
process.stdout.write(JSON.stringify({ ok: false, code: 'GIT_FAILED', message: '创建贮藏失败;请刷新确认当前状态', diagnostics: String(error.stderr || error.message) }));
|
|
1215
|
+
} finally {
|
|
1216
|
+
if (temporary) fs.rmSync(temporary, { recursive: true, force: true });
|
|
1217
|
+
}
|
|
1218
|
+
`;
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
|
|
775
1222
|
// src/host/git-repository-service.ts
|
|
1223
|
+
async function runShell(shell, specification) {
|
|
1224
|
+
if (shell.execute) return (await shell.execute(specification)).result();
|
|
1225
|
+
if (shell.run) return shell.run(specification);
|
|
1226
|
+
throw new Error("shell \u670D\u52A1\u4E0D\u652F\u6301\u6267\u884C\u547D\u4EE4");
|
|
1227
|
+
}
|
|
776
1228
|
function errorResult(code, message, diagnostics, reason) {
|
|
777
1229
|
return {
|
|
778
1230
|
ok: false,
|
|
@@ -791,13 +1243,17 @@ function mutationErrorCode(result) {
|
|
|
791
1243
|
}
|
|
792
1244
|
function parseStatus(status) {
|
|
793
1245
|
const files = [];
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
const
|
|
799
|
-
const
|
|
800
|
-
|
|
1246
|
+
const records = status.split("\0");
|
|
1247
|
+
for (let index = 0; index < records.length - 1; index++) {
|
|
1248
|
+
const record = records[index];
|
|
1249
|
+
if (record.startsWith("## ") || record.length < 4) continue;
|
|
1250
|
+
const indexStatus = record[0];
|
|
1251
|
+
const workTreeStatus = record[1];
|
|
1252
|
+
const path = record.slice(3);
|
|
1253
|
+
if (/[RC]/.test(indexStatus + workTreeStatus)) {
|
|
1254
|
+
if (index + 1 >= records.length - 1) break;
|
|
1255
|
+
files.push({ indexStatus, workTreeStatus, path, originalPath: records[++index] });
|
|
1256
|
+
} else files.push({ indexStatus, workTreeStatus, path });
|
|
801
1257
|
}
|
|
802
1258
|
return files;
|
|
803
1259
|
}
|
|
@@ -821,6 +1277,9 @@ function conflictCount(files) {
|
|
|
821
1277
|
function validCommitHash(hash) {
|
|
822
1278
|
return typeof hash === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(hash);
|
|
823
1279
|
}
|
|
1280
|
+
function validStashPath(path) {
|
|
1281
|
+
return typeof path === "string" && path.length > 0 && path.length <= 4096 && !path.includes("\0") && !path.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(path) && !path.split(/[\\/]/).some((part) => part === "..");
|
|
1282
|
+
}
|
|
824
1283
|
function parseCommitFiles(nameStatus, numstat) {
|
|
825
1284
|
const statusRows = nameStatus.split("\n").filter(Boolean);
|
|
826
1285
|
const statRows = numstat.split("\n").filter(Boolean);
|
|
@@ -859,10 +1318,12 @@ function parseCommitFiles(nameStatus, numstat) {
|
|
|
859
1318
|
totals: { files: statusRows.length, additions, deletions, binary }
|
|
860
1319
|
};
|
|
861
1320
|
}
|
|
862
|
-
var MUTATION_OUTPUT_MAX_CHARS, DIFF_MAX_CHARS, COMMIT_DETAIL_MAX_CHARS, COMMIT_DIFF_MAX_CHARS, COMMIT_FILE_MAX, STASH_MAX, MAX_PATHS, OPERATION_TTL_MS, GitRepositoryService;
|
|
1321
|
+
var MUTATION_OUTPUT_MAX_CHARS, DIFF_MAX_CHARS, COMMIT_DETAIL_MAX_CHARS, COMMIT_DIFF_MAX_CHARS, COMMIT_FILE_MAX, STASH_MAX, MAX_PATHS, OPERATION_TTL_MS, repositoryLocks, GitRepositoryService;
|
|
863
1322
|
var init_git_repository_service = __esm({
|
|
864
1323
|
"src/host/git-repository-service.ts"() {
|
|
865
1324
|
"use strict";
|
|
1325
|
+
init_conflict_worker();
|
|
1326
|
+
init_stash_worker();
|
|
866
1327
|
init_command_policy();
|
|
867
1328
|
MUTATION_OUTPUT_MAX_CHARS = 1e5;
|
|
868
1329
|
DIFF_MAX_CHARS = 2e5;
|
|
@@ -872,17 +1333,18 @@ var init_git_repository_service = __esm({
|
|
|
872
1333
|
STASH_MAX = 100;
|
|
873
1334
|
MAX_PATHS = 100;
|
|
874
1335
|
OPERATION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1336
|
+
repositoryLocks = /* @__PURE__ */ new Map();
|
|
875
1337
|
GitRepositoryService = class {
|
|
876
1338
|
constructor(shell) {
|
|
877
1339
|
this.shell = shell;
|
|
878
1340
|
}
|
|
879
|
-
locks =
|
|
1341
|
+
locks = repositoryLocks;
|
|
880
1342
|
operations = /* @__PURE__ */ new Map();
|
|
881
1343
|
async run(workdir, command, timeoutMs = 2e4, stdoutMaxBytes = 3e4, signal, sandboxPolicy) {
|
|
882
1344
|
if (!this.shell) return { exitCode: -1, stderr: { text: "shell \u670D\u52A1\u4E0D\u53EF\u7528" } };
|
|
883
1345
|
try {
|
|
884
1346
|
const specification = this.shell.resolve({ command, workdir, timeoutMs, stdoutMaxBytes, signal, ...sandboxPolicy ? { sandboxPolicy } : {} });
|
|
885
|
-
return await this.shell
|
|
1347
|
+
return await runShell(this.shell, specification);
|
|
886
1348
|
} catch (error) {
|
|
887
1349
|
return { exitCode: -1, stderr: { text: error instanceof Error ? error.message : String(error) } };
|
|
888
1350
|
}
|
|
@@ -901,13 +1363,14 @@ var init_git_repository_service = __esm({
|
|
|
901
1363
|
const [branchResult, headResult, statusResult] = await Promise.all([
|
|
902
1364
|
this.run(workdir, "git branch --show-current", 15e3, 4096, signal, sandboxPolicy),
|
|
903
1365
|
this.run(workdir, "git rev-parse --short HEAD", 15e3, 4096, signal, sandboxPolicy),
|
|
904
|
-
this.run(workdir, "git status --porcelain=v1 --branch --untracked-files=all", 15e3, 5e4, signal, sandboxPolicy)
|
|
1366
|
+
this.run(workdir, "git status --porcelain=v1 --branch --untracked-files=all -z", 15e3, 5e4, signal, sandboxPolicy)
|
|
905
1367
|
]);
|
|
906
1368
|
if (branchResult.exitCode !== 0 || headResult.exitCode !== 0 || statusResult.exitCode !== 0) {
|
|
907
1369
|
return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6 Git \u4ED3\u5E93\u6458\u8981", redactAndLimit(outputOf(branchResult) + "\n" + outputOf(headResult) + "\n" + outputOf(statusResult), 8192));
|
|
908
1370
|
}
|
|
909
|
-
const
|
|
910
|
-
const files = parseStatus(
|
|
1371
|
+
const rawStatus = statusResult.stdout?.text ?? "";
|
|
1372
|
+
const files = parseStatus(rawStatus);
|
|
1373
|
+
const status = redactAndLimit(rawStatus.replace(/\0/g, "\n"), 5e4);
|
|
911
1374
|
return {
|
|
912
1375
|
ok: true,
|
|
913
1376
|
data: {
|
|
@@ -944,7 +1407,7 @@ var init_git_repository_service = __esm({
|
|
|
944
1407
|
async getBranches(workdir, signal, sandboxPolicy) {
|
|
945
1408
|
const topLevel = await this.getTopLevel(workdir, signal, sandboxPolicy);
|
|
946
1409
|
if (!topLevel.ok) return topLevel;
|
|
947
|
-
const branchFormat = "%(HEAD)%09%(refname:
|
|
1410
|
+
const branchFormat = "%(HEAD)%09%(refname:lstrip=2)%09%(upstream:short)";
|
|
948
1411
|
const referenceFormat = "%(refname)%09%(refname:short)%09%(objectname:short)%09%(*objectname:short)%09%(subject)";
|
|
949
1412
|
const [branchResult, referenceResult] = await Promise.all([
|
|
950
1413
|
this.run(workdir, "git branch --format=" + quoteShellArg(branchFormat), 15e3, 3e4, signal, sandboxPolicy),
|
|
@@ -962,7 +1425,9 @@ var init_git_repository_service = __esm({
|
|
|
962
1425
|
for (const line of (referenceResult.stdout?.text ?? "").split("\n").filter(Boolean)) {
|
|
963
1426
|
const [fullName = "", shortName = "", objectHash = "", peeledHash = "", subject = ""] = line.split(" ");
|
|
964
1427
|
const entry = {
|
|
965
|
-
|
|
1428
|
+
// Git's :short adds heads/ or remotes/ when references collide.
|
|
1429
|
+
// The UI already distinguishes namespaces, so keep the actual name.
|
|
1430
|
+
name: redactAndLimit(fullName.startsWith("refs/remotes/") ? fullName.slice(13) : shortName, 512),
|
|
966
1431
|
hash: redactAndLimit(peeledHash || objectHash, 128),
|
|
967
1432
|
subject: redactAndLimit(subject, 4096)
|
|
968
1433
|
};
|
|
@@ -1101,6 +1566,102 @@ var init_git_repository_service = __esm({
|
|
|
1101
1566
|
});
|
|
1102
1567
|
return { ok: true, data: stashes };
|
|
1103
1568
|
}
|
|
1569
|
+
async resolveStash(workdir, selector, hash, signal, sandboxPolicy) {
|
|
1570
|
+
if (typeof selector !== "string" || !/^stash@\{\d{1,9}\}$/.test(selector) || !validCommitHash(hash)) {
|
|
1571
|
+
return errorResult("INVALID_ARGUMENT", "\u5FC5\u987B\u63D0\u4F9B\u6709\u6548\u7684\u8D2E\u85CF\u7F16\u53F7\u548C\u5B8C\u6574\u54C8\u5E0C");
|
|
1572
|
+
}
|
|
1573
|
+
const root = await this.getTopLevel(workdir, signal, sandboxPolicy);
|
|
1574
|
+
if (!root.ok) return root;
|
|
1575
|
+
const resolved = await this.run(root.data.topLevel, "git rev-parse --verify " + quoteShellArg(selector), 15e3, 4096, signal, sandboxPolicy);
|
|
1576
|
+
if (resolved.exitCode !== 0 || (resolved.stdout?.text ?? "").trim() !== hash) {
|
|
1577
|
+
return errorResult("STATE_CONFLICT", "\u8D2E\u85CF\u5217\u8868\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u65B0\u9009\u62E9");
|
|
1578
|
+
}
|
|
1579
|
+
const metadata = await this.run(root.data.topLevel, "git show -s --format=%P " + quoteShellArg(hash), 15e3, 4096, signal, sandboxPolicy);
|
|
1580
|
+
const parents = (metadata.stdout?.text ?? "").trim().split(" ").filter(validCommitHash);
|
|
1581
|
+
if (metadata.exitCode !== 0 || parents.length < 2) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u8D2E\u85CF\u7236\u8282\u70B9");
|
|
1582
|
+
return { ok: true, data: { topLevel: root.data.topLevel, hash, parents } };
|
|
1583
|
+
}
|
|
1584
|
+
async getStashDetail(workdir, selector, hash, signal, sandboxPolicy) {
|
|
1585
|
+
const stash = await this.resolveStash(workdir, selector, hash, signal, sandboxPolicy);
|
|
1586
|
+
if (!stash.ok) return stash;
|
|
1587
|
+
const { topLevel, parents } = stash.data;
|
|
1588
|
+
const commands = ["git diff --no-ext-diff --no-textconv --no-renames --name-status -z " + quoteShellArg(parents[0]) + " " + quoteShellArg(stash.data.hash) + " --"];
|
|
1589
|
+
if (parents[2]) commands.push("git diff-tree --root --no-commit-id --no-renames --name-status -r -z " + quoteShellArg(parents[2]) + " --");
|
|
1590
|
+
const results = await Promise.all(commands.map((command) => this.run(topLevel, command, 2e4, COMMIT_DETAIL_MAX_CHARS, signal, sandboxPolicy)));
|
|
1591
|
+
const failure = results.find((result) => result.exitCode !== 0);
|
|
1592
|
+
if (failure) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u8D2E\u85CF\u6587\u4EF6", redactAndLimit(outputOf(failure), 8192));
|
|
1593
|
+
const files = [];
|
|
1594
|
+
let filesTruncated = false;
|
|
1595
|
+
results.forEach((result, index) => {
|
|
1596
|
+
const raw = result.stdout?.text ?? "";
|
|
1597
|
+
const parts = raw.split("\0");
|
|
1598
|
+
if (raw.length >= COMMIT_DETAIL_MAX_CHARS || raw && !raw.endsWith("\0")) filesTruncated = true;
|
|
1599
|
+
for (let i = 0; i + 2 < parts.length; i += 2) {
|
|
1600
|
+
if (files.length >= COMMIT_FILE_MAX) {
|
|
1601
|
+
filesTruncated = true;
|
|
1602
|
+
break;
|
|
1603
|
+
}
|
|
1604
|
+
files.push({ status: parts[i], path: parts[i + 1], untracked: index === 1 });
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
return { ok: true, data: { hash: stash.data.hash, files, filesTruncated } };
|
|
1608
|
+
}
|
|
1609
|
+
async getStashDiff(workdir, selector, hash, path, untracked, signal, sandboxPolicy) {
|
|
1610
|
+
if (!validStashPath(path)) return errorResult("INVALID_ARGUMENT", "\u8BF7\u9009\u62E9\u4ED3\u5E93\u5185\u7684\u6587\u4EF6\u8DEF\u5F84");
|
|
1611
|
+
const stash = await this.resolveStash(workdir, selector, hash, signal, sandboxPolicy);
|
|
1612
|
+
if (!stash.ok) return stash;
|
|
1613
|
+
const { topLevel, parents } = stash.data;
|
|
1614
|
+
if (untracked && !parents[2]) return errorResult("INVALID_ARGUMENT", "\u8BE5\u8D2E\u85CF\u4E0D\u5305\u542B\u672A\u8DDF\u8E2A\u6587\u4EF6");
|
|
1615
|
+
const common = "--no-color --no-ext-diff --no-textconv --no-renames --patch ";
|
|
1616
|
+
const command = untracked ? "git --literal-pathspecs diff-tree --root --no-commit-id -r " + common + quoteShellArg(parents[2]) : "git --literal-pathspecs diff " + common + quoteShellArg(parents[0]) + " " + quoteShellArg(stash.data.hash);
|
|
1617
|
+
const result = await this.run(topLevel, command + " -- " + quoteShellArg(path), 3e4, DIFF_MAX_CHARS + 1024, signal, sandboxPolicy);
|
|
1618
|
+
if (result.exitCode !== 0) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u8D2E\u85CF Diff", redactAndLimit(outputOf(result), 8192));
|
|
1619
|
+
const raw = redactSecrets(result.stdout?.text ?? "");
|
|
1620
|
+
return { ok: true, data: { path, staged: false, diff: raw.slice(0, DIFF_MAX_CHARS), truncated: raw.length > DIFF_MAX_CHARS } };
|
|
1621
|
+
}
|
|
1622
|
+
async createStash(request, message, paths, includeUntracked) {
|
|
1623
|
+
if (typeof message !== "string" || message.length > 4096 || message.includes("\0")) return errorResult("INVALID_ARGUMENT", "\u8D2E\u85CF\u8BF4\u660E\u4E0D\u80FD\u8D85\u8FC7 4096 \u5B57\u7B26\u6216\u5305\u542B\u7A7A\u5B57\u7B26");
|
|
1624
|
+
if (paths !== void 0 && (!Array.isArray(paths) || !paths.length || paths.length > 500 || !paths.every(validStashPath))) {
|
|
1625
|
+
return errorResult("INVALID_ARGUMENT", "\u8BF7\u9009\u62E9 1\u2013500 \u4E2A\u6587\u4EF6\uFF0C\u6216\u8D2E\u85CF\u5168\u90E8\u6587\u4EF6");
|
|
1626
|
+
}
|
|
1627
|
+
return this.withMutation(request, async () => {
|
|
1628
|
+
const result = await this.run(request.workdir, createStashCommand(message, paths, includeUntracked), 12e4, MUTATION_OUTPUT_MAX_CHARS, request.signal, request.sandboxPolicy);
|
|
1629
|
+
if (result.exitCode !== 0) return errorResult(mutationErrorCode(result), "\u521B\u5EFA\u8D2E\u85CF\u5931\u8D25\uFF0C\u8BF7\u5237\u65B0\u786E\u8BA4\u5F53\u524D\u72B6\u6001", redactAndLimit(outputOf(result), 8192));
|
|
1630
|
+
try {
|
|
1631
|
+
const response = JSON.parse(result.stdout?.text ?? "");
|
|
1632
|
+
if (!response.ok) return errorResult(response.code, response.message, redactAndLimit(response.diagnostics ?? "", 8192));
|
|
1633
|
+
} catch {
|
|
1634
|
+
return errorResult("GIT_FAILED", "\u65E0\u6CD5\u786E\u8BA4\u8D2E\u85CF\u521B\u5EFA\u7ED3\u679C\uFF0C\u8BF7\u5237\u65B0");
|
|
1635
|
+
}
|
|
1636
|
+
const summary = await this.getSummary(request.workdir, request.signal, request.sandboxPolicy);
|
|
1637
|
+
return summary.ok ? { ...summary, operationId: String(request.operationId) } : summary;
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
async mutateStash(request, action, selector, hash, name, confirmRisk = false) {
|
|
1641
|
+
if (action === "drop-stash" && !confirmRisk) return errorResult("PERMISSION_DENIED", "\u5220\u9664\u8D2E\u85CF\u524D\u5FC5\u987B\u786E\u8BA4\u5176\u4E2D\u7684\u6539\u52A8\u53EF\u80FD\u4E22\u5931");
|
|
1642
|
+
if (action === "branch-stash" && !validBranchName(name)) return errorResult("INVALID_ARGUMENT", "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u65B0\u5206\u652F\u540D");
|
|
1643
|
+
return this.withMutation(request, async () => {
|
|
1644
|
+
const stash = await this.resolveStash(request.workdir, selector, hash, request.signal, request.sandboxPolicy);
|
|
1645
|
+
if (!stash.ok) return stash;
|
|
1646
|
+
const root = stash.data.topLevel;
|
|
1647
|
+
if (action !== "drop-stash") {
|
|
1648
|
+
const state = await this.conflictAction("get-conflicts", root, {}, void 0, request.sandboxPolicy);
|
|
1649
|
+
if (!state.ok) return state;
|
|
1650
|
+
const data = state.data;
|
|
1651
|
+
if (data.operation || data.files.length) return errorResult("STATE_CONFLICT", "\u8BF7\u5148\u5B8C\u6210\u5F53\u524D Git \u64CD\u4F5C\u5E76\u89E3\u51B3\u51B2\u7A81");
|
|
1652
|
+
if (action === "branch-stash") {
|
|
1653
|
+
const summary2 = await this.getSummary(root, request.signal, request.sandboxPolicy);
|
|
1654
|
+
if (!summary2.ok) return summary2;
|
|
1655
|
+
if (summary2.data.files.length) return errorResult("STATE_CONFLICT", "\u4ECE\u8D2E\u85CF\u521B\u5EFA\u5206\u652F\u524D\uFF0C\u8BF7\u5148\u63D0\u4EA4\u6216\u8D2E\u85CF\u5F53\u524D\u6539\u52A8", void 0, "DIRTY_WORKTREE");
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
const command = action === "branch-stash" ? "git -c apply.whitespace=nowarn stash branch " + quoteShellArg(name) + " " + quoteShellArg(selector) : "git stash " + { "apply-stash": "apply", "pop-stash": "pop", "drop-stash": "drop" }[action] + " " + quoteShellArg(action === "apply-stash" ? stash.data.hash : selector);
|
|
1659
|
+
const result = await this.run(root, command, 12e4, MUTATION_OUTPUT_MAX_CHARS, request.signal, request.sandboxPolicy);
|
|
1660
|
+
if (result.exitCode !== 0) return errorResult(mutationErrorCode(result), "\u8D2E\u85CF\u64CD\u4F5C\u672A\u5B8C\u6210\uFF1B\u5982\u6709\u51B2\u7A81\uFF0C\u8BF7\u5728\u201C\u51B2\u7A81\u89E3\u51B3\u201D\u4E2D\u5904\u7406\u3002\u5931\u8D25\u7684\u5F39\u51FA\u4F1A\u4FDD\u7559\u8D2E\u85CF\u3002", redactAndLimit(outputOf(result), 8192));
|
|
1661
|
+
const summary = await this.getSummary(root, request.signal, request.sandboxPolicy);
|
|
1662
|
+
return summary.ok ? { ...summary, operationId: String(request.operationId) } : summary;
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1104
1665
|
async getSyncState(workdir, signal, sandboxPolicy) {
|
|
1105
1666
|
const summary = await this.getSummary(workdir, signal, sandboxPolicy);
|
|
1106
1667
|
if (!summary.ok) return summary;
|
|
@@ -1276,6 +1837,40 @@ var init_git_repository_service = __esm({
|
|
|
1276
1837
|
return this.mutateAndRead(request, command, failureMessage, false, () => this.getSyncState(request.workdir, request.signal, request.sandboxPolicy));
|
|
1277
1838
|
}
|
|
1278
1839
|
async mutateAndRead(request, command, failureMessage, requiresStagedContent, readResult) {
|
|
1840
|
+
return this.withMutation(request, async () => {
|
|
1841
|
+
if (requiresStagedContent) {
|
|
1842
|
+
const staged = await this.run(request.workdir, "git diff --cached --quiet", 15e3, 4096, request.signal, request.sandboxPolicy);
|
|
1843
|
+
if (staged.exitCode === 0) return errorResult("STATE_CONFLICT", "\u6CA1\u6709\u5DF2\u6682\u5B58\u7684\u6539\u52A8\uFF0C\u65E0\u6CD5\u63D0\u4EA4");
|
|
1844
|
+
if (staged.exitCode !== 1) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u68C0\u67E5\u6682\u5B58\u533A", redactAndLimit(outputOf(staged), 8192));
|
|
1845
|
+
}
|
|
1846
|
+
const executed = await this.run(request.workdir, command, 12e4, MUTATION_OUTPUT_MAX_CHARS, request.signal, request.sandboxPolicy);
|
|
1847
|
+
if (executed.exitCode !== 0) return errorResult(mutationErrorCode(executed), failureMessage, redactAndLimit(outputOf(executed), 8192));
|
|
1848
|
+
const result = await readResult();
|
|
1849
|
+
return result.ok ? { ...result, operationId: String(request.operationId) } : result;
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
async conflictAction(action, workdir, payload, request, sandboxPolicy) {
|
|
1853
|
+
if (action === "amend-message" && (typeof payload.message !== "string" || Buffer.byteLength(payload.message) > 48 * 1024)) {
|
|
1854
|
+
return errorResult("INVALID_ARGUMENT", "\u63D0\u4EA4\u8BF4\u660E\u5FC5\u987B\u662F 48 KiB \u4EE5\u5185\u7684\u6587\u672C");
|
|
1855
|
+
}
|
|
1856
|
+
if (action === "save-conflict" && (typeof payload.content !== "string" || Buffer.byteLength(payload.content) > 48 * 1024)) {
|
|
1857
|
+
return errorResult("INVALID_ARGUMENT", "\u7ED3\u679C\u5FC5\u987B\u662F 48 KiB \u4EE5\u5185\u7684\u6587\u672C");
|
|
1858
|
+
}
|
|
1859
|
+
const run = async () => {
|
|
1860
|
+
const result = await this.run(workdir, conflictWorkerCommand(action, payload), 12e4, 2 * 1024 * 1024, request?.signal, request?.sandboxPolicy ?? sandboxPolicy);
|
|
1861
|
+
if (result.exitCode !== 0) return errorResult(mutationErrorCode(result), "\u51B2\u7A81\u64CD\u4F5C\u5931\u8D25", redactAndLimit(outputOf(result), 8192));
|
|
1862
|
+
try {
|
|
1863
|
+
const response = JSON.parse(result.stdout?.text ?? "");
|
|
1864
|
+
if (!response.ok && response.diagnostics) response.diagnostics = redactAndLimit(response.diagnostics, 8192);
|
|
1865
|
+
if (!response.ok) response.message = redactAndLimit(response.message, 8192);
|
|
1866
|
+
return response;
|
|
1867
|
+
} catch {
|
|
1868
|
+
return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u5B8C\u6574\u7684\u51B2\u7A81\u6570\u636E");
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
return request ? this.withMutation(request, run) : run();
|
|
1872
|
+
}
|
|
1873
|
+
async withMutation(request, task) {
|
|
1279
1874
|
if (!request.sessionId || !request.workdir) return errorResult("SESSION_NOT_FOUND", "\u65E0\u6CD5\u786E\u5B9A\u5F53\u524D\u4F1A\u8BDD\u7684\u4ED3\u5E93\u76EE\u5F55");
|
|
1280
1875
|
if (typeof request.operationId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(request.operationId)) {
|
|
1281
1876
|
return errorResult("INVALID_ARGUMENT", "operationId \u5FC5\u987B\u662F 1\u2013128 \u4E2A\u5B89\u5168\u5B57\u7B26");
|
|
@@ -1286,7 +1881,7 @@ var init_git_repository_service = __esm({
|
|
|
1286
1881
|
const key = request.sessionId + "\0" + repository.data.topLevel + "\0" + request.operationId;
|
|
1287
1882
|
const existing = this.operations.get(key);
|
|
1288
1883
|
if (existing) return existing.result;
|
|
1289
|
-
const lockKey =
|
|
1884
|
+
const lockKey = repository.data.topLevel;
|
|
1290
1885
|
const previous = this.locks.get(lockKey) ?? Promise.resolve();
|
|
1291
1886
|
let release = () => {
|
|
1292
1887
|
};
|
|
@@ -1297,15 +1892,7 @@ var init_git_repository_service = __esm({
|
|
|
1297
1892
|
this.locks.set(lockKey, queued);
|
|
1298
1893
|
const result = previous.then(async () => {
|
|
1299
1894
|
try {
|
|
1300
|
-
|
|
1301
|
-
const staged = await this.run(request.workdir, "git diff --cached --quiet", 15e3, 4096, request.signal, request.sandboxPolicy);
|
|
1302
|
-
if (staged.exitCode === 0) return errorResult("STATE_CONFLICT", "\u6CA1\u6709\u5DF2\u6682\u5B58\u7684\u6539\u52A8\uFF0C\u65E0\u6CD5\u63D0\u4EA4");
|
|
1303
|
-
if (staged.exitCode !== 1) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u68C0\u67E5\u6682\u5B58\u533A", redactAndLimit(outputOf(staged), 8192));
|
|
1304
|
-
}
|
|
1305
|
-
const executed = await this.run(request.workdir, command, 12e4, MUTATION_OUTPUT_MAX_CHARS, request.signal, request.sandboxPolicy);
|
|
1306
|
-
if (executed.exitCode !== 0) return errorResult(mutationErrorCode(executed), failureMessage, redactAndLimit(outputOf(executed), 8192));
|
|
1307
|
-
const result2 = await readResult();
|
|
1308
|
-
return result2.ok ? { ...result2, operationId: String(request.operationId) } : result2;
|
|
1895
|
+
return await task();
|
|
1309
1896
|
} finally {
|
|
1310
1897
|
release();
|
|
1311
1898
|
if (this.locks.get(lockKey) === queued) this.locks.delete(lockKey);
|
|
@@ -1358,23 +1945,36 @@ var require_plugin = __commonJS({
|
|
|
1358
1945
|
}
|
|
1359
1946
|
return void 0;
|
|
1360
1947
|
}
|
|
1361
|
-
function repositoryContextForSession(ctx, sandboxPolicy, sessionId) {
|
|
1948
|
+
async function repositoryContextForSession(ctx, sandboxPolicy, sessionId) {
|
|
1362
1949
|
try {
|
|
1363
1950
|
const agents = ctx.get("agents");
|
|
1364
1951
|
const agent = agents && agents.get(sessionId);
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1952
|
+
const session = agent?.session ?? ctx.get("sessions")?.get(sessionId);
|
|
1953
|
+
if (session) {
|
|
1954
|
+
const workdir = sessionWorkdir({ agent: { session } }, {}, ctx);
|
|
1955
|
+
if (!workdir) return null;
|
|
1956
|
+
return { workdir, policy: sandboxPolicy?.resolve({ session }) };
|
|
1957
|
+
}
|
|
1958
|
+
const query = ctx.get("sessionQuery");
|
|
1959
|
+
if (!query) return null;
|
|
1960
|
+
const observation = await query.observeSession(sessionId);
|
|
1961
|
+
try {
|
|
1962
|
+
const workdir = observation.header.cwd;
|
|
1963
|
+
if (!workdir || !observation.projections) return null;
|
|
1964
|
+
const mode = observation.projections.values.sandboxMode ?? void 0;
|
|
1965
|
+
const policy = sandboxPolicy ? { ...sandboxPolicy.resolve({ mode }), workspaceRoot: workdir, sessionId } : void 0;
|
|
1966
|
+
return { workdir, policy };
|
|
1967
|
+
} finally {
|
|
1968
|
+
observation[Symbol.dispose]();
|
|
1969
|
+
}
|
|
1970
|
+
} catch (error) {
|
|
1371
1971
|
return null;
|
|
1372
1972
|
}
|
|
1373
1973
|
}
|
|
1374
1974
|
async function runGit(shell, workdir, command, timeoutMs, stdoutMaxBytes, signal, policy) {
|
|
1375
1975
|
try {
|
|
1376
1976
|
const spec = shell.resolve({ command, workdir, timeoutMs, stdoutMaxBytes, signal, ...policy ? { sandboxPolicy: policy } : {} });
|
|
1377
|
-
return await shell
|
|
1977
|
+
return await runShell(shell, spec);
|
|
1378
1978
|
} catch (err) {
|
|
1379
1979
|
return { exitCode: -1, signal: null, timedOut: false, aborted: false, stdout: { text: "" }, stderr: { text: errorMessage2(err) } };
|
|
1380
1980
|
}
|
|
@@ -1962,7 +2562,7 @@ var require_plugin = __commonJS({
|
|
|
1962
2562
|
}
|
|
1963
2563
|
});
|
|
1964
2564
|
}
|
|
1965
|
-
const registerWebServer = (webServer) => registerEasyGitActions(webServer, {
|
|
2565
|
+
const registerWebServer = (webServer, connection) => registerEasyGitActions(webServer, {
|
|
1966
2566
|
repository,
|
|
1967
2567
|
proposalStorageReady,
|
|
1968
2568
|
shell,
|
|
@@ -1987,16 +2587,16 @@ var require_plugin = __commonJS({
|
|
|
1987
2587
|
errorCode,
|
|
1988
2588
|
reason
|
|
1989
2589
|
),
|
|
1990
|
-
resolveExecutionPolicy: (sessionId) => {
|
|
1991
|
-
const
|
|
1992
|
-
|
|
1993
|
-
return
|
|
2590
|
+
resolveExecutionPolicy: async (sessionId) => {
|
|
2591
|
+
const context = await repositoryContextForSession(ctx, sandboxPolicy, sessionId);
|
|
2592
|
+
if (!context) throw new Error("\u65E0\u6CD5\u786E\u5B9A\u5F53\u524D\u4F1A\u8BDD\u7684\u4ED3\u5E93\u76EE\u5F55");
|
|
2593
|
+
return context.policy;
|
|
1994
2594
|
}
|
|
1995
|
-
});
|
|
2595
|
+
}, connection);
|
|
1996
2596
|
if (typeof ctx.inject === "function") {
|
|
1997
|
-
ctx.inject(["webServer"], (webCtx) => registerWebServer(webCtx.get("webServer")));
|
|
2597
|
+
ctx.inject(["webServer", "connection"], (webCtx) => registerWebServer(webCtx.get("webServer"), webCtx.get("connection")));
|
|
1998
2598
|
} else {
|
|
1999
|
-
registerWebServer(ctx.get("webServer"));
|
|
2599
|
+
registerWebServer(ctx.get("webServer"), ctx.get("connection"));
|
|
2000
2600
|
}
|
|
2001
2601
|
}
|
|
2002
2602
|
};
|