780 lines
24 KiB
JavaScript
780 lines
24 KiB
JavaScript
function createMockRepositoryBridge(context) {
|
|
const { wait, clone, iso, storage, repositoryListeners, operationListeners, updateListeners, emitRepositories, emitOperations, randomSha, now, profile, state, repositories, recompute, snapshot, commitHistory, advanceOperation, syncState, diffs, findRepo, findProfileRepo, branchesByRepo, stashesByRepo, updateOperation } = context;
|
|
return {
|
|
async bootstrap() {
|
|
await wait(80);
|
|
snapshot();
|
|
return {
|
|
appVersion: "0.10.15-demo",
|
|
platform: "win32",
|
|
state: clone(state),
|
|
git: { available: true, version: "git version 2.47.3" },
|
|
diagnostics: {
|
|
enabled: true,
|
|
level: state.preferences.diagnosticLevel,
|
|
retentionDays: state.preferences.logRetentionDays,
|
|
maxFileMb: state.preferences.maxLogFileMb,
|
|
directory: "<HOME>/AppData/Roaming/ForgeFlow/diagnostics",
|
|
fileCount: 2,
|
|
totalBytes: 18432,
|
|
totalSize: "18.0 KB",
|
|
latestAt: iso(-2000),
|
|
lastWriteError: null,
|
|
},
|
|
};
|
|
},
|
|
async selectDirectory() {
|
|
await wait();
|
|
return "C:\\Development";
|
|
},
|
|
async selectKeyFile() {
|
|
await wait();
|
|
return "C:\\Users\\your-name\\.ssh\\id_ed25519";
|
|
},
|
|
async setupPreflight({ baseUrl, token, roots = [] }) {
|
|
await wait(240);
|
|
const checks = [
|
|
{
|
|
id: "git.available",
|
|
label: "Git command line",
|
|
status: "pass",
|
|
detail: "git version 2.47.3",
|
|
required: true,
|
|
},
|
|
{
|
|
id: "git.identity",
|
|
label: "Git author identity",
|
|
status: "pass",
|
|
detail: "Jens <jens@example.invalid>",
|
|
required: false,
|
|
},
|
|
{
|
|
id: "storage.userdata",
|
|
label: "Application data storage",
|
|
status: "pass",
|
|
detail: "ForgeFlow can write its local configuration.",
|
|
required: true,
|
|
},
|
|
{
|
|
id: "storage.diagnostics",
|
|
label: "Diagnostic log storage",
|
|
status: "pass",
|
|
detail: "The diagnostic directory is writable.",
|
|
required: true,
|
|
},
|
|
{
|
|
id: "storage.credentials",
|
|
label: "Protected credential storage",
|
|
status: "pass",
|
|
detail: "The operating system can encrypt the Gitea token at rest.",
|
|
required: false,
|
|
},
|
|
{
|
|
id: "workspace.roots",
|
|
label: "Development folders",
|
|
status: roots.length ? "pass" : "warning",
|
|
detail: roots.length
|
|
? `${roots.length} folder(s) selected.`
|
|
: "No development folder selected yet.",
|
|
required: false,
|
|
},
|
|
{
|
|
id: "gitea.connection",
|
|
label: "Gitea connection",
|
|
status: baseUrl && token ? "pass" : "warning",
|
|
detail:
|
|
baseUrl && token
|
|
? "Connection parameters are ready for validation."
|
|
: "Enter the Gitea URL and token.",
|
|
required: false,
|
|
},
|
|
];
|
|
return {
|
|
kind: "system",
|
|
startedAt: iso(-100),
|
|
completedAt: iso(),
|
|
checks,
|
|
summary: {
|
|
counts: {
|
|
pass: checks.filter((i) => i.status === "pass").length,
|
|
warning: checks.filter((i) => i.status === "warning").length,
|
|
fail: 0,
|
|
skipped: 0,
|
|
},
|
|
blocking: [],
|
|
ready: true,
|
|
},
|
|
};
|
|
},
|
|
async validateGitea({ baseUrl, token }) {
|
|
await wait(320);
|
|
if (!baseUrl || !token)
|
|
throw new Error("Enter an instance URL and access token.");
|
|
return {
|
|
baseUrl: baseUrl.replace(/\/$/, ""),
|
|
user: { login: "jens", full_name: "Jens" },
|
|
repositoryCount: repositories.length,
|
|
version: "1.26.0",
|
|
};
|
|
},
|
|
async completeSetup(payload) {
|
|
await wait(300);
|
|
state.setupComplete = true;
|
|
state.gitea = {
|
|
baseUrl: payload.baseUrl,
|
|
user: payload.user,
|
|
hasToken: true,
|
|
};
|
|
state.workspaceRoots = payload.workspaceRoots;
|
|
storage.set("forgeflow-demo-setup", "true");
|
|
return { state: clone(state), tokenState: { persistent: true } };
|
|
},
|
|
async updateGitea(payload) {
|
|
const validation = await this.validateGitea({
|
|
...payload,
|
|
token: payload.token || "preserved-demo-token",
|
|
});
|
|
state.gitea = {
|
|
baseUrl: validation.baseUrl,
|
|
user: validation.user,
|
|
hasToken: true,
|
|
};
|
|
return {
|
|
validation,
|
|
tokenState: { persistent: true, preserved: !payload.token },
|
|
state: clone(state),
|
|
};
|
|
},
|
|
async setWorkspaceRoots(roots) {
|
|
state.workspaceRoots = [...new Set(roots)];
|
|
return clone(state);
|
|
},
|
|
async setAppearance(appearance) {
|
|
state.appearance = appearance;
|
|
storage.set("forgeflow-theme", appearance);
|
|
return clone(state);
|
|
},
|
|
async setPreferences(preferences) {
|
|
state.preferences = { ...state.preferences, ...preferences };
|
|
snapshot();
|
|
return clone(state);
|
|
},
|
|
async setUpdatePreferences(updates) {
|
|
state.updates = { ...state.updates, ...updates };
|
|
return clone(state);
|
|
},
|
|
async checkForUpdates() {
|
|
await wait(300);
|
|
return {
|
|
checkedAt: iso(),
|
|
owner: state.updates.owner,
|
|
repo: state.updates.repo,
|
|
branch: state.updates.branch,
|
|
currentVersion: "0.5.4",
|
|
remoteVersion: "0.6.0",
|
|
remoteSha: "a".repeat(40),
|
|
shortSha: "aaaaaaa",
|
|
available: true,
|
|
mode: "source",
|
|
};
|
|
},
|
|
async downloadUpdate() {
|
|
await wait(500);
|
|
return {
|
|
...(await this.checkForUpdates()),
|
|
downloaded: true,
|
|
archivePath: "C:\\Temp\\ForgeFlow-0.4.1.zip",
|
|
sha256: "b".repeat(64),
|
|
};
|
|
},
|
|
async applyUpdate() {
|
|
await wait(200);
|
|
return { launched: true, confirmed: true, version: "0.6.0" };
|
|
},
|
|
async saveServer(server) {
|
|
const saved = {
|
|
...server,
|
|
id: server.id || `server-${Date.now()}`,
|
|
hasPassword: server.authType === "password",
|
|
hasPassphrase: false,
|
|
};
|
|
state.servers = [
|
|
saved,
|
|
...state.servers.filter((item) => item.id !== saved.id),
|
|
];
|
|
return { server: clone(saved), state: clone(state) };
|
|
},
|
|
async deleteServer(serverId) {
|
|
state.servers = state.servers.filter((item) => item.id !== serverId);
|
|
return clone(state);
|
|
},
|
|
async testServer(serverId) {
|
|
const server = state.servers.find((item) => item.id === serverId);
|
|
server.hostFingerprint = server.hostFingerprint || "SHA256:demo";
|
|
return {
|
|
connected: true,
|
|
fingerprint: server.hostFingerprint,
|
|
server: clone(server),
|
|
output: "Linux\n/usr/bin/git\nDocker Compose version v2",
|
|
state: clone(state),
|
|
};
|
|
},
|
|
async inspectServerProject() {
|
|
return {
|
|
exists: true,
|
|
rootGit: true,
|
|
head: "d42d4a7".padEnd(40, "0"),
|
|
branch: "main",
|
|
trackedChanges: [],
|
|
composeFiles: ["docker-compose.yml"],
|
|
nestedGit: ["source"],
|
|
dockerfile: true,
|
|
};
|
|
},
|
|
async refreshRepositories() {
|
|
await wait(260);
|
|
return snapshot();
|
|
},
|
|
async discoverRepositories() {
|
|
await wait(360);
|
|
return snapshot()
|
|
.filter((repo) => repo.localPath)
|
|
.map((repo) => ({
|
|
localPath: repo.localPath,
|
|
remoteUrl: repo.cloneUrl,
|
|
status: repo.localStatus,
|
|
}));
|
|
},
|
|
async favoriteRepository(fullName, favorite) {
|
|
const key = fullName.toLowerCase();
|
|
state.favorites = favorite
|
|
? [...new Set([...state.favorites, key])]
|
|
: state.favorites.filter((item) => item !== key);
|
|
snapshot();
|
|
return clone(state);
|
|
},
|
|
async linkRepository(fullName, localPath) {
|
|
const repo = repositories.find((item) => item.fullName === fullName);
|
|
repo.localPath = localPath;
|
|
repo.linkState = "linked";
|
|
repo.localStatus = makeStatus({ head: randomSha() });
|
|
emitRepositories();
|
|
return snapshot();
|
|
},
|
|
async unlinkRepository(fullName) {
|
|
const repo = repositories.find((item) => item.fullName === fullName);
|
|
repo.localPath = null;
|
|
repo.localStatus = null;
|
|
repo.linkState = "remote-only";
|
|
emitRepositories();
|
|
return snapshot();
|
|
},
|
|
async repositoryStatus(localPath) {
|
|
return clone(findRepo(localPath)?.localStatus);
|
|
},
|
|
async repositoryDiff(localPath, filePath) {
|
|
await wait(80);
|
|
return (
|
|
diffs[filePath] ||
|
|
`diff --git a/${filePath} b/${filePath}\n--- a/${filePath}\n+++ b/${filePath}\n@@ -1 +1 @@\n-old\n+new`
|
|
);
|
|
},
|
|
async repositoryDiffHunks(localPath, filePath) {
|
|
const diff = await this.repositoryDiff(localPath, filePath);
|
|
return {
|
|
filePath,
|
|
partialSupported: true,
|
|
hunks: [
|
|
{
|
|
index: 0,
|
|
heading: "@@ -1 +1 @@",
|
|
additions: 1,
|
|
deletions: 1,
|
|
lines: diff.split("\n").slice(-4),
|
|
},
|
|
],
|
|
};
|
|
},
|
|
async stageHunks(localPath, filePath) {
|
|
return this.stageFiles(localPath, [filePath]);
|
|
},
|
|
async conflictState(localPath) {
|
|
const repo = findRepo(localPath);
|
|
const files = repo.localStatus.files
|
|
.filter((item) => item.conflict)
|
|
.map((item) => item.path);
|
|
return {
|
|
operation: files.length ? "merge" : null,
|
|
files,
|
|
canContinue: false,
|
|
status: clone(repo.localStatus),
|
|
};
|
|
},
|
|
async resolveConflict(localPath, filePath) {
|
|
const repo = findRepo(localPath);
|
|
const file = repo.localStatus.files.find(
|
|
(item) => item.path === filePath,
|
|
);
|
|
if (file) {
|
|
file.conflict = false;
|
|
file.staged = true;
|
|
file.unstaged = false;
|
|
}
|
|
recompute(repo);
|
|
return this.conflictState(localPath);
|
|
},
|
|
async continueGitOperation(localPath) {
|
|
return this.conflictState(localPath);
|
|
},
|
|
async abortGitOperation(localPath) {
|
|
return this.conflictState(localPath);
|
|
},
|
|
async stageFiles(localPath, files) {
|
|
const repo = findRepo(localPath);
|
|
repo.localStatus.files.forEach((item) => {
|
|
if (!files?.length || files.includes(item.path)) {
|
|
item.staged = true;
|
|
item.unstaged = false;
|
|
item.indexCode = item.untracked ? "A" : "M";
|
|
item.worktreeCode = ".";
|
|
}
|
|
});
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return clone(repo.localStatus);
|
|
},
|
|
async unstageFiles(localPath, files) {
|
|
const repo = findRepo(localPath);
|
|
repo.localStatus.files.forEach((item) => {
|
|
if (!files?.length || files.includes(item.path)) {
|
|
item.staged = false;
|
|
item.unstaged = true;
|
|
item.indexCode = ".";
|
|
item.worktreeCode = item.untracked ? "?" : "M";
|
|
}
|
|
});
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return clone(repo.localStatus);
|
|
},
|
|
async commit(localPath, message, files) {
|
|
await wait(520);
|
|
if (!message?.trim()) throw new Error("Enter a commit message.");
|
|
const repo = findRepo(localPath);
|
|
repo.localStatus.files = repo.localStatus.files.filter(
|
|
(item) => !files?.includes(item.path),
|
|
);
|
|
repo.localStatus.head = randomSha();
|
|
repo.localStatus.branch.ahead += 1;
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return {
|
|
commitOutput: `[${repo.localStatus.branch.head} ${repo.localStatus.shortHead}] ${message}`,
|
|
commitSha: repo.localStatus.head,
|
|
status: clone(repo.localStatus),
|
|
};
|
|
},
|
|
async commitAndPush(localPath, message, files) {
|
|
const result = await this.commit(localPath, message, files);
|
|
const repo = findRepo(localPath);
|
|
await wait(240);
|
|
repo.localStatus.branch.ahead = 0;
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return {
|
|
...result,
|
|
pushOutput: "Push completed.",
|
|
status: clone(repo.localStatus),
|
|
};
|
|
},
|
|
async commitStaged(localPath, message) {
|
|
const repo = findRepo(localPath);
|
|
return this.commit(
|
|
localPath,
|
|
message,
|
|
repo.localStatus.files
|
|
.filter((item) => item.staged)
|
|
.map((item) => item.path),
|
|
);
|
|
},
|
|
async commitStagedAndPush(localPath, message) {
|
|
const repo = findRepo(localPath);
|
|
return this.commitAndPush(
|
|
localPath,
|
|
message,
|
|
repo.localStatus.files
|
|
.filter((item) => item.staged)
|
|
.map((item) => item.path),
|
|
);
|
|
},
|
|
async push(localPath) {
|
|
await wait(360);
|
|
const repo = findRepo(localPath);
|
|
repo.localStatus.branch.ahead = 0;
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return { output: "Push completed.", status: clone(repo.localStatus) };
|
|
},
|
|
async fetch() {
|
|
await wait(260);
|
|
return { output: "Fetch completed." };
|
|
},
|
|
async pull(localPath) {
|
|
await wait(380);
|
|
const repo = findRepo(localPath);
|
|
repo.localStatus.branch.behind = 0;
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return { output: "Fast-forwarded.", status: clone(repo.localStatus) };
|
|
},
|
|
async previewWorkspaceSync(localPath) {
|
|
await wait(260);
|
|
const repo = findRepo(localPath);
|
|
const status = repo.localStatus;
|
|
const targetSha = status.branch.behind ? "f".repeat(40) : status.head;
|
|
return {
|
|
id: `demo-${String(status.head).slice(0, 7)}-${status.branch.ahead}-${status.branch.behind}`.padEnd(64, "0").slice(0, 64),
|
|
branch: status.branch.head,
|
|
upstream: status.branch.upstream || `origin/${status.branch.head}`,
|
|
currentSha: status.head,
|
|
targetSha,
|
|
needsSync: !status.clean || status.head !== targetSha || status.branch.ahead > 0,
|
|
blockers: [],
|
|
summary: {
|
|
resultingTrackedChanges: status.branch.behind ? 3 : 0,
|
|
added: status.branch.behind ? 1 : 0,
|
|
modified: status.branch.behind ? 1 : 0,
|
|
deleted: status.branch.behind ? 1 : 0,
|
|
renamed: 0,
|
|
localFilesToStash: status.counts.changed,
|
|
untrackedFilesToStash: status.counts.untracked,
|
|
localCommitsToProtect: status.branch.ahead,
|
|
incomingCommits: status.branch.behind,
|
|
},
|
|
changes: status.branch.behind
|
|
? [
|
|
{ code: "A", status: "added", path: "src/remote-feature.js" },
|
|
{ code: "M", status: "modified", path: "README.md" },
|
|
{ code: "D", status: "deleted", path: "docs/obsolete.md" },
|
|
]
|
|
: [],
|
|
localFiles: clone(status.files),
|
|
incomingCommits: [],
|
|
localCommits: [],
|
|
recovery: {
|
|
safetyBranch: status.branch.ahead > 0,
|
|
stash: status.counts.changed > 0,
|
|
untrackedCleanup: status.counts.untracked > 0,
|
|
ignoredFilesPreserved: true,
|
|
},
|
|
};
|
|
},
|
|
async applyWorkspaceSync(localPath, expectedPlanId) {
|
|
const plan = await this.previewWorkspaceSync(localPath);
|
|
if (plan.id !== expectedPlanId) throw new Error("The workspace sync preview is stale.");
|
|
const repo = findRepo(localPath);
|
|
const hadChanges = repo.localStatus.counts.changed > 0;
|
|
repo.localStatus.head = plan.targetSha;
|
|
repo.localStatus.shortHead = plan.targetSha.slice(0, 7);
|
|
repo.localStatus.files = [];
|
|
repo.localStatus.branch.ahead = 0;
|
|
repo.localStatus.branch.behind = 0;
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return {
|
|
applied: plan.needsSync,
|
|
unchanged: !plan.needsSync,
|
|
plan,
|
|
status: clone(repo.localStatus),
|
|
backupBranch: plan.summary.localCommitsToProtect ? `forgeflow/recovery-${plan.branch}-demo` : null,
|
|
stash: hadChanges ? { ref: "stash@{0}", shortSha: "demo123", subject: "ForgeFlow workspace sync" } : null,
|
|
ignoredFilesPreserved: true,
|
|
cleaned: [],
|
|
};
|
|
},
|
|
async history() {
|
|
await wait(100);
|
|
return clone(commitHistory);
|
|
},
|
|
async branchProtection(fullName, branch) {
|
|
return {
|
|
branch,
|
|
protected: branch === "main",
|
|
requiredApprovals: branch === "main" ? 1 : 0,
|
|
requireSignedCommits: false,
|
|
};
|
|
},
|
|
async pullRequests() {
|
|
return [
|
|
{
|
|
number: 42,
|
|
title: "Harden deployment preflight",
|
|
html_url: "https://gitea.internal/jens/vacancyradar/pulls/42",
|
|
created_at: iso(-7_200_000),
|
|
updated_at: iso(-900_000),
|
|
head: { ref: "feature/deployment-api" },
|
|
base: { ref: "main" },
|
|
},
|
|
];
|
|
},
|
|
async createPullRequest(fullName, title, body, base) {
|
|
return {
|
|
number: 42,
|
|
title,
|
|
body,
|
|
base,
|
|
html_url: `https://gitea.internal/${fullName}/pulls/42`,
|
|
};
|
|
},
|
|
async branches(localPath) {
|
|
const repo = findRepo(localPath);
|
|
if (!branchesByRepo.has(localPath))
|
|
branchesByRepo.set(localPath, [
|
|
{
|
|
name: repo.localStatus.branch.head,
|
|
current: true,
|
|
sha: repo.localStatus.head,
|
|
shortSha: repo.localStatus.shortHead,
|
|
upstream: repo.localStatus.branch.upstream,
|
|
},
|
|
{
|
|
name: "main",
|
|
current: repo.localStatus.branch.head === "main",
|
|
sha: repo.localStatus.head,
|
|
shortSha: repo.localStatus.shortHead,
|
|
upstream: "origin/main",
|
|
},
|
|
]);
|
|
return clone(branchesByRepo.get(localPath));
|
|
},
|
|
async checkoutBranch(localPath, branch) {
|
|
const repo = findRepo(localPath);
|
|
if (!repo.localStatus.clean)
|
|
throw new Error(
|
|
"Commit or stash local changes before switching branches.",
|
|
);
|
|
const list = await this.branches(localPath);
|
|
list.forEach((item) => {
|
|
item.current = item.name === branch;
|
|
});
|
|
branchesByRepo.set(localPath, list);
|
|
repo.localStatus.branch.head = branch;
|
|
repo.localStatus.branch.upstream = `origin/${branch}`;
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return { status: clone(repo.localStatus), branches: clone(list) };
|
|
},
|
|
async createBranch(localPath, branch) {
|
|
const repo = findRepo(localPath);
|
|
const list = await this.branches(localPath);
|
|
list.forEach((item) => {
|
|
item.current = false;
|
|
});
|
|
list.unshift({
|
|
name: branch,
|
|
current: true,
|
|
sha: repo.localStatus.head,
|
|
shortSha: repo.localStatus.shortHead,
|
|
upstream: null,
|
|
});
|
|
branchesByRepo.set(localPath, list);
|
|
repo.localStatus.branch.head = branch;
|
|
repo.localStatus.branch.upstream = null;
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return { status: clone(repo.localStatus), branches: clone(list) };
|
|
},
|
|
async stash(localPath, message) {
|
|
const repo = findRepo(localPath);
|
|
const list = stashesByRepo.get(localPath) || [];
|
|
list.unshift({
|
|
ref: `stash@{${list.length}}`,
|
|
subject: message || "ForgeFlow stash",
|
|
date: iso(),
|
|
});
|
|
stashesByRepo.set(localPath, list);
|
|
repo.localStatus.files = [];
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return {
|
|
output: "Saved working directory and index state.",
|
|
status: clone(repo.localStatus),
|
|
stashes: clone(list),
|
|
};
|
|
},
|
|
async stashList(localPath) {
|
|
return clone(stashesByRepo.get(localPath) || []);
|
|
},
|
|
async popStash(localPath, ref) {
|
|
const repo = findRepo(localPath);
|
|
const list = stashesByRepo.get(localPath) || [];
|
|
const index = list.findIndex((item) => item.ref === ref);
|
|
if (index < 0) throw new Error("Stash not found.");
|
|
list.splice(index, 1);
|
|
stashesByRepo.set(localPath, list);
|
|
repo.localStatus.files = [makeFile("src/restored-from-stash.ts")];
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return {
|
|
output: "Stash applied.",
|
|
status: clone(repo.localStatus),
|
|
stashes: clone(list),
|
|
};
|
|
},
|
|
async gitRecoveryStatus(localPath) {
|
|
const repo = findRepo(localPath);
|
|
const status = clone(repo.localStatus);
|
|
const upstream = status.branch?.upstream;
|
|
const recommendations = [
|
|
{
|
|
id: "fetch",
|
|
label: "Fetch and recalculate remote state",
|
|
action: "fetch",
|
|
safe: true,
|
|
},
|
|
];
|
|
if (
|
|
status.clean &&
|
|
status.branch.behind > 0 &&
|
|
status.branch.ahead === 0 &&
|
|
upstream
|
|
) {
|
|
recommendations.push({
|
|
id: "pull",
|
|
label: `Fast-forward from ${upstream}`,
|
|
action: "fast-forward",
|
|
safe: true,
|
|
});
|
|
}
|
|
if (
|
|
status.branch.ahead > 0 &&
|
|
status.branch.behind === 0 &&
|
|
upstream
|
|
) {
|
|
recommendations.push({
|
|
id: "push",
|
|
label: `Push ${status.branch.ahead} local commit(s)`,
|
|
action: "push",
|
|
safe: true,
|
|
});
|
|
}
|
|
return {
|
|
status,
|
|
lockReport: {
|
|
root: localPath,
|
|
gitDir: `${localPath}\\.git`,
|
|
locks: [],
|
|
processes: { available: true, active: [] },
|
|
},
|
|
recommendations,
|
|
};
|
|
},
|
|
async reconcileRepository(localPath) {
|
|
await wait(160);
|
|
return this.gitRecoveryStatus(localPath);
|
|
},
|
|
async repairGitLocks(localPath) {
|
|
return {
|
|
...(await this.gitRecoveryStatus(localPath)).lockReport,
|
|
removed: [],
|
|
skipped: [],
|
|
repaired: false,
|
|
};
|
|
},
|
|
async repairRepositorySync(localPath, strategy) {
|
|
const repo = findRepo(localPath);
|
|
if (strategy === "fast-forward") {
|
|
repo.localStatus.branch.behind = 0;
|
|
repo.localStatus.head = "f".repeat(40);
|
|
} else if (strategy === "push") {
|
|
repo.localStatus.branch.ahead = 0;
|
|
} else if (strategy !== "fetch") {
|
|
throw new Error("Unsupported demo synchronization strategy.");
|
|
}
|
|
recompute(repo);
|
|
emitRepositories();
|
|
return {
|
|
strategy,
|
|
backupBranch: null,
|
|
status: clone(repo.localStatus),
|
|
lockReport: (await this.gitRecoveryStatus(localPath)).lockReport,
|
|
};
|
|
},
|
|
async indexLockInfo() {
|
|
return { exists: false, ageMs: 0 };
|
|
},
|
|
async repairIndexLock() {
|
|
return { removed: true };
|
|
},
|
|
async setOrigin(localPath, remoteUrl) {
|
|
const repo = findRepo(localPath);
|
|
repo.localStatus.remoteUrl = remoteUrl;
|
|
repo.sshUrl = remoteUrl;
|
|
emitRepositories();
|
|
return clone(repo.localStatus);
|
|
},
|
|
async normalizeOrigins() {
|
|
const changes = [];
|
|
repositories
|
|
.filter((repo) => repo.localPath && repo.sshUrl)
|
|
.forEach((repo) => {
|
|
if (repo.localStatus.remoteUrl !== repo.sshUrl) {
|
|
changes.push({
|
|
fullName: repo.fullName,
|
|
previous: repo.localStatus.remoteUrl,
|
|
next: repo.sshUrl,
|
|
});
|
|
repo.localStatus.remoteUrl = repo.sshUrl;
|
|
}
|
|
});
|
|
emitRepositories();
|
|
return { changes, repositories: snapshot() };
|
|
},
|
|
async cloneRepository(fullName, mode = "default") {
|
|
await wait(620);
|
|
const repository = repositories.find(
|
|
(item) => item.fullName === fullName,
|
|
);
|
|
if (!repository) throw new Error("Repository not found.");
|
|
if (repository.localPath)
|
|
throw new Error("This repository already has a linked local folder.");
|
|
const root =
|
|
mode === "custom" ? "D:\\OtherProjects" : state.workspaceRoots[0];
|
|
if (!root) return { cancelled: true };
|
|
const target = `${root.replace(/[\\/]+$/, "")}\\${repository.name}`;
|
|
const head = randomSha();
|
|
repository.localPath = target;
|
|
repository.localStatus = makeStatus({
|
|
head,
|
|
branch: repository.defaultBranch || "main",
|
|
});
|
|
repository.localStatus.root = target;
|
|
repository.localStatus.remoteUrl =
|
|
repository.preferredCloneUrl || repository.cloneUrl;
|
|
repository.linkState = "linked";
|
|
recompute(repository);
|
|
const current = snapshot();
|
|
emitRepositories();
|
|
return {
|
|
target,
|
|
status: clone(repository.localStatus),
|
|
reused: false,
|
|
repositories: current,
|
|
state: clone(state),
|
|
};
|
|
},
|
|
async openPath() {
|
|
return true;
|
|
},
|
|
async openEditor() {
|
|
return { launched: true, executable: "code" };
|
|
},
|
|
async openTerminal() {
|
|
return { launched: true, executable: "wt.exe" };
|
|
},
|
|
async openExternal() {
|
|
return true;
|
|
},
|
|
};
|
|
}
|