Files
ForgeFlow/src/renderer/mock-repository-bridge.js
T
NuklearRabbit f8c505e525
ForgeFlow quality gate / quality (push) Canceled after 0s
docs: refresh user guide and screenshots
2026-07-30 02:02:18 +02:00

636 lines
20 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.2-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\\Jens\\.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 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 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;
},
};
}