@@ -454,210 +454,6 @@ export function registerAppHandlers() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle("list-versions", async (_, { appId }: { appId: number }) => {
|
|
||||||
const app = await db.query.apps.findFirst({
|
|
||||||
where: eq(apps.id, appId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!app) {
|
|
||||||
throw new Error("App not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const appPath = getDyadAppPath(app.path);
|
|
||||||
|
|
||||||
// Just return an empty array if the app is not a git repo.
|
|
||||||
if (!fs.existsSync(path.join(appPath, ".git"))) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const commits = await git.log({
|
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
// KEEP UP TO DATE WITH ChatHeader.tsx
|
|
||||||
depth: 10_000, // Limit to last 10_000 commits for performance
|
|
||||||
});
|
|
||||||
|
|
||||||
return commits.map((commit) => ({
|
|
||||||
oid: commit.oid,
|
|
||||||
message: commit.commit.message,
|
|
||||||
timestamp: commit.commit.author.timestamp,
|
|
||||||
})) satisfies Version[];
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error(`Error listing versions for app ${appId}:`, error);
|
|
||||||
throw new Error(`Failed to list versions: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
"revert-version",
|
|
||||||
async (
|
|
||||||
_,
|
|
||||||
{ appId, previousVersionId }: { appId: number; previousVersionId: string }
|
|
||||||
) => {
|
|
||||||
return withLock(appId, async () => {
|
|
||||||
const app = await db.query.apps.findFirst({
|
|
||||||
where: eq(apps.id, appId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!app) {
|
|
||||||
throw new Error("App not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const appPath = getDyadAppPath(app.path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await git.checkout({
|
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
ref: "main",
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
|
|
||||||
const matrix = await git.statusMatrix({
|
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
ref: previousVersionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process each file to revert to the state in previousVersionId
|
|
||||||
for (const [
|
|
||||||
filepath,
|
|
||||||
headStatus,
|
|
||||||
workdirStatus,
|
|
||||||
stageStatus,
|
|
||||||
] of matrix) {
|
|
||||||
const fullPath = path.join(appPath, filepath);
|
|
||||||
|
|
||||||
// If file exists in HEAD (previous version)
|
|
||||||
if (headStatus === 1) {
|
|
||||||
// If file doesn't exist or has changed in working directory, restore it from the target commit
|
|
||||||
if (workdirStatus !== 1) {
|
|
||||||
const { blob } = await git.readBlob({
|
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
oid: previousVersionId,
|
|
||||||
filepath,
|
|
||||||
});
|
|
||||||
await fsPromises.mkdir(path.dirname(fullPath), {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
await fsPromises.writeFile(fullPath, Buffer.from(blob));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If file doesn't exist in HEAD but exists in working directory, delete it
|
|
||||||
else if (headStatus === 0 && workdirStatus !== 0) {
|
|
||||||
if (fs.existsSync(fullPath)) {
|
|
||||||
await fsPromises.unlink(fullPath);
|
|
||||||
await git.remove({
|
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
filepath: filepath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stage all changes
|
|
||||||
await git.add({
|
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
filepath: ".",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a revert commit
|
|
||||||
await git.commit({
|
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
message: `Reverted all changes back to version ${previousVersionId}`,
|
|
||||||
author: await getGitAuthor(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find the chat and message associated with the commit hash
|
|
||||||
const messageWithCommit = await db.query.messages.findFirst({
|
|
||||||
where: eq(messages.commitHash, previousVersionId),
|
|
||||||
with: {
|
|
||||||
chat: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// If we found a message with this commit hash, delete all subsequent messages (but keep this message)
|
|
||||||
if (messageWithCommit) {
|
|
||||||
const chatId = messageWithCommit.chatId;
|
|
||||||
|
|
||||||
// Find all messages in this chat with IDs > the one with our commit hash
|
|
||||||
const messagesToDelete = await db.query.messages.findMany({
|
|
||||||
where: and(
|
|
||||||
eq(messages.chatId, chatId),
|
|
||||||
gt(messages.id, messageWithCommit.id)
|
|
||||||
),
|
|
||||||
orderBy: desc(messages.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
`Deleting ${messagesToDelete.length} messages after commit ${previousVersionId} from chat ${chatId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete the messages
|
|
||||||
if (messagesToDelete.length > 0) {
|
|
||||||
await db
|
|
||||||
.delete(messages)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(messages.chatId, chatId),
|
|
||||||
gt(messages.id, messageWithCommit.id)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error(
|
|
||||||
`Error reverting to version ${previousVersionId} for app ${appId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
throw new Error(`Failed to revert version: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
"checkout-version",
|
|
||||||
async (_, { appId, versionId }: { appId: number; versionId: string }) => {
|
|
||||||
return withLock(appId, async () => {
|
|
||||||
const app = await db.query.apps.findFirst({
|
|
||||||
where: eq(apps.id, appId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!app) {
|
|
||||||
throw new Error("App not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const appPath = getDyadAppPath(app.path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Checkout the target commit
|
|
||||||
await git.checkout({
|
|
||||||
fs,
|
|
||||||
dir: appPath,
|
|
||||||
ref: versionId,
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error(
|
|
||||||
`Error checking out version ${versionId} for app ${appId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
throw new Error(`Failed to checkout version: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"edit-app-file",
|
"edit-app-file",
|
||||||
async (
|
async (
|
||||||
|
|||||||
222
src/ipc/handlers/version_handlers.ts
Normal file
222
src/ipc/handlers/version_handlers.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { ipcMain } from "electron";
|
||||||
|
import { db } from "../../db";
|
||||||
|
import { apps, messages } from "../../db/schema";
|
||||||
|
import { desc, eq, and, gt } from "drizzle-orm";
|
||||||
|
import type { Version } from "../ipc_types";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { getDyadAppPath } from "../../paths/paths";
|
||||||
|
import git from "isomorphic-git";
|
||||||
|
import { promises as fsPromises } from "node:fs";
|
||||||
|
// Import our utility modules
|
||||||
|
import { withLock } from "../utils/lock_utils";
|
||||||
|
import { getGitAuthor } from "../utils/git_author";
|
||||||
|
import log from "electron-log";
|
||||||
|
|
||||||
|
const logger = log.scope("version_handlers");
|
||||||
|
|
||||||
|
export function registerVersionHandlers() {
|
||||||
|
ipcMain.handle("list-versions", async (_, { appId }: { appId: number }) => {
|
||||||
|
const app = await db.query.apps.findFirst({
|
||||||
|
where: eq(apps.id, appId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw new Error("App not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const appPath = getDyadAppPath(app.path);
|
||||||
|
|
||||||
|
// Just return an empty array if the app is not a git repo.
|
||||||
|
if (!fs.existsSync(path.join(appPath, ".git"))) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const commits = await git.log({
|
||||||
|
fs,
|
||||||
|
dir: appPath,
|
||||||
|
// KEEP UP TO DATE WITH ChatHeader.tsx
|
||||||
|
depth: 10_000, // Limit to last 10_000 commits for performance
|
||||||
|
});
|
||||||
|
|
||||||
|
return commits.map((commit) => ({
|
||||||
|
oid: commit.oid,
|
||||||
|
message: commit.commit.message,
|
||||||
|
timestamp: commit.commit.author.timestamp,
|
||||||
|
})) satisfies Version[];
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`Error listing versions for app ${appId}:`, error);
|
||||||
|
throw new Error(`Failed to list versions: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
"revert-version",
|
||||||
|
async (
|
||||||
|
_,
|
||||||
|
{ appId, previousVersionId }: { appId: number; previousVersionId: string }
|
||||||
|
) => {
|
||||||
|
return withLock(appId, async () => {
|
||||||
|
const app = await db.query.apps.findFirst({
|
||||||
|
where: eq(apps.id, appId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw new Error("App not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const appPath = getDyadAppPath(app.path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await git.checkout({
|
||||||
|
fs,
|
||||||
|
dir: appPath,
|
||||||
|
ref: "main",
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
|
||||||
|
const matrix = await git.statusMatrix({
|
||||||
|
fs,
|
||||||
|
dir: appPath,
|
||||||
|
ref: previousVersionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process each file to revert to the state in previousVersionId
|
||||||
|
for (const [
|
||||||
|
filepath,
|
||||||
|
headStatus,
|
||||||
|
workdirStatus,
|
||||||
|
stageStatus,
|
||||||
|
] of matrix) {
|
||||||
|
const fullPath = path.join(appPath, filepath);
|
||||||
|
|
||||||
|
// If file exists in HEAD (previous version)
|
||||||
|
if (headStatus === 1) {
|
||||||
|
// If file doesn't exist or has changed in working directory, restore it from the target commit
|
||||||
|
if (workdirStatus !== 1) {
|
||||||
|
const { blob } = await git.readBlob({
|
||||||
|
fs,
|
||||||
|
dir: appPath,
|
||||||
|
oid: previousVersionId,
|
||||||
|
filepath,
|
||||||
|
});
|
||||||
|
await fsPromises.mkdir(path.dirname(fullPath), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
await fsPromises.writeFile(fullPath, Buffer.from(blob));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If file doesn't exist in HEAD but exists in working directory, delete it
|
||||||
|
else if (headStatus === 0 && workdirStatus !== 0) {
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
await fsPromises.unlink(fullPath);
|
||||||
|
await git.remove({
|
||||||
|
fs,
|
||||||
|
dir: appPath,
|
||||||
|
filepath: filepath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage all changes
|
||||||
|
await git.add({
|
||||||
|
fs,
|
||||||
|
dir: appPath,
|
||||||
|
filepath: ".",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a revert commit
|
||||||
|
await git.commit({
|
||||||
|
fs,
|
||||||
|
dir: appPath,
|
||||||
|
message: `Reverted all changes back to version ${previousVersionId}`,
|
||||||
|
author: await getGitAuthor(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the chat and message associated with the commit hash
|
||||||
|
const messageWithCommit = await db.query.messages.findFirst({
|
||||||
|
where: eq(messages.commitHash, previousVersionId),
|
||||||
|
with: {
|
||||||
|
chat: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we found a message with this commit hash, delete all subsequent messages (but keep this message)
|
||||||
|
if (messageWithCommit) {
|
||||||
|
const chatId = messageWithCommit.chatId;
|
||||||
|
|
||||||
|
// Find all messages in this chat with IDs > the one with our commit hash
|
||||||
|
const messagesToDelete = await db.query.messages.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(messages.chatId, chatId),
|
||||||
|
gt(messages.id, messageWithCommit.id)
|
||||||
|
),
|
||||||
|
orderBy: desc(messages.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
`Deleting ${messagesToDelete.length} messages after commit ${previousVersionId} from chat ${chatId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete the messages
|
||||||
|
if (messagesToDelete.length > 0) {
|
||||||
|
await db
|
||||||
|
.delete(messages)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(messages.chatId, chatId),
|
||||||
|
gt(messages.id, messageWithCommit.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(
|
||||||
|
`Error reverting to version ${previousVersionId} for app ${appId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error(`Failed to revert version: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
"checkout-version",
|
||||||
|
async (_, { appId, versionId }: { appId: number; versionId: string }) => {
|
||||||
|
return withLock(appId, async () => {
|
||||||
|
const app = await db.query.apps.findFirst({
|
||||||
|
where: eq(apps.id, appId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw new Error("App not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const appPath = getDyadAppPath(app.path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Checkout the target commit
|
||||||
|
await git.checkout({
|
||||||
|
fs,
|
||||||
|
dir: appPath,
|
||||||
|
ref: versionId,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(
|
||||||
|
`Error checking out version ${versionId} for app ${appId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error(`Failed to checkout version: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import { registerLocalModelHandlers } from "./handlers/local_model_handlers";
|
|||||||
import { registerTokenCountHandlers } from "./handlers/token_count_handlers";
|
import { registerTokenCountHandlers } from "./handlers/token_count_handlers";
|
||||||
import { registerWindowHandlers } from "./handlers/window_handlers";
|
import { registerWindowHandlers } from "./handlers/window_handlers";
|
||||||
import { registerUploadHandlers } from "./handlers/upload_handlers";
|
import { registerUploadHandlers } from "./handlers/upload_handlers";
|
||||||
|
import { registerVersionHandlers } from "./handlers/version_handlers";
|
||||||
export function registerIpcHandlers() {
|
export function registerIpcHandlers() {
|
||||||
// Register all IPC handlers by category
|
// Register all IPC handlers by category
|
||||||
registerAppHandlers();
|
registerAppHandlers();
|
||||||
@@ -31,4 +31,5 @@ export function registerIpcHandlers() {
|
|||||||
registerTokenCountHandlers();
|
registerTokenCountHandlers();
|
||||||
registerWindowHandlers();
|
registerWindowHandlers();
|
||||||
registerUploadHandlers();
|
registerUploadHandlers();
|
||||||
|
registerVersionHandlers();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user