Support native Git (experimental) (#338)
This commit is contained in:
@@ -23,7 +23,7 @@ import { getEnvVar } from "../utils/read_env";
|
||||
import { readSettings } from "../../main/settings";
|
||||
|
||||
import fixPath from "fix-path";
|
||||
import { getGitAuthor } from "../utils/git_author";
|
||||
|
||||
import killPort from "kill-port";
|
||||
import util from "util";
|
||||
import log from "electron-log";
|
||||
@@ -33,6 +33,7 @@ import { getLanguageModelProviders } from "../shared/language_model_helpers";
|
||||
import { startProxy } from "../utils/start_proxy_server";
|
||||
import { Worker } from "worker_threads";
|
||||
import { createFromTemplate } from "./createFromTemplate";
|
||||
import { gitCommit } from "../utils/git_utils";
|
||||
|
||||
const logger = log.scope("app_handlers");
|
||||
const handle = createLoggedHandler(logger);
|
||||
@@ -207,11 +208,9 @@ export function registerAppHandlers() {
|
||||
});
|
||||
|
||||
// Create initial commit
|
||||
const commitHash = await git.commit({
|
||||
fs: fs,
|
||||
dir: fullAppPath,
|
||||
const commitHash = await gitCommit({
|
||||
path: fullAppPath,
|
||||
message: "Init Dyad app",
|
||||
author: await getGitAuthor(),
|
||||
});
|
||||
|
||||
// Update chat with initial commit hash
|
||||
@@ -521,11 +520,9 @@ export function registerAppHandlers() {
|
||||
filepath: filePath,
|
||||
});
|
||||
|
||||
await git.commit({
|
||||
fs,
|
||||
dir: appPath,
|
||||
await gitCommit({
|
||||
path: appPath,
|
||||
message: `Updated ${filePath}`,
|
||||
author: await getGitAuthor(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ import { db } from "@/db";
|
||||
import { chats } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import git from "isomorphic-git";
|
||||
import { getGitAuthor } from "../utils/git_author";
|
||||
|
||||
import { ImportAppParams, ImportAppResult } from "../ipc_types";
|
||||
import { copyDirectoryRecursive } from "../utils/file_utils";
|
||||
import { gitCommit } from "../utils/git_utils";
|
||||
|
||||
const logger = log.scope("import-handlers");
|
||||
const handle = createLoggedHandler(logger);
|
||||
@@ -114,11 +115,9 @@ export function registerImportHandlers() {
|
||||
});
|
||||
|
||||
// Create initial commit
|
||||
await git.commit({
|
||||
fs: fs,
|
||||
dir: destPath,
|
||||
await gitCommit({
|
||||
path: destPath,
|
||||
message: "Init Dyad app",
|
||||
author: await getGitAuthor(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@ import type { Version, BranchResult } 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 git, { type ReadCommitResult } from "isomorphic-git";
|
||||
import { withLock } from "../utils/lock_utils";
|
||||
import { getGitAuthor } from "../utils/git_author";
|
||||
import log from "electron-log";
|
||||
import { createLoggedHandler } from "./safe_handle";
|
||||
import { gitCheckout, gitCommit, gitStageToRevert } from "../utils/git_utils";
|
||||
|
||||
const logger = log.scope("version_handlers");
|
||||
|
||||
@@ -40,7 +39,7 @@ export function registerVersionHandlers() {
|
||||
depth: 10_000, // Limit to last 10_000 commits for performance
|
||||
});
|
||||
|
||||
return commits.map((commit) => ({
|
||||
return commits.map((commit: ReadCommitResult) => ({
|
||||
oid: commit.oid,
|
||||
message: commit.commit.message,
|
||||
timestamp: commit.commit.author.timestamp,
|
||||
@@ -102,65 +101,19 @@ export function registerVersionHandlers() {
|
||||
|
||||
const appPath = getDyadAppPath(app.path);
|
||||
|
||||
await git.checkout({
|
||||
fs,
|
||||
dir: appPath,
|
||||
await gitCheckout({
|
||||
path: 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] 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: ".",
|
||||
await gitStageToRevert({
|
||||
path: appPath,
|
||||
targetOid: previousVersionId,
|
||||
});
|
||||
|
||||
// Create a revert commit
|
||||
await git.commit({
|
||||
fs,
|
||||
dir: appPath,
|
||||
await gitCommit({
|
||||
path: appPath,
|
||||
message: `Reverted all changes back to version ${previousVersionId}`,
|
||||
author: await getGitAuthor(),
|
||||
});
|
||||
|
||||
// Find the chat and message associated with the commit hash
|
||||
@@ -221,9 +174,8 @@ export function registerVersionHandlers() {
|
||||
|
||||
const appPath = getDyadAppPath(app.path);
|
||||
|
||||
await git.checkout({
|
||||
fs,
|
||||
dir: appPath,
|
||||
await gitCheckout({
|
||||
path: appPath,
|
||||
ref: versionId,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import { getDyadAppPath } from "../../paths/paths";
|
||||
import path from "node:path";
|
||||
import git from "isomorphic-git";
|
||||
|
||||
import { getGitAuthor } from "../utils/git_author";
|
||||
import log from "electron-log";
|
||||
import { executeAddDependency } from "./executeAddDependency";
|
||||
import {
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
} from "../../supabase_admin/supabase_management_client";
|
||||
import { isServerFunction } from "../../supabase_admin/supabase_utils";
|
||||
import { SqlQuery } from "../../lib/schemas";
|
||||
import { gitCommit } from "../utils/git_utils";
|
||||
|
||||
const readFile = fs.promises.readFile;
|
||||
const logger = log.scope("response_processor");
|
||||
@@ -460,11 +460,9 @@ export async function processFullResponseActions(
|
||||
? `[dyad] ${chatSummary} - ${changes.join(", ")}`
|
||||
: `[dyad] ${changes.join(", ")}`;
|
||||
// Use chat summary, if provided, or default for commit message
|
||||
let commitHash = await git.commit({
|
||||
fs,
|
||||
dir: appPath,
|
||||
let commitHash = await gitCommit({
|
||||
path: appPath,
|
||||
message,
|
||||
author: await getGitAuthor(),
|
||||
});
|
||||
logger.log(`Successfully committed changes: ${changes.join(", ")}`);
|
||||
|
||||
@@ -482,11 +480,9 @@ export async function processFullResponseActions(
|
||||
filepath: ".",
|
||||
});
|
||||
try {
|
||||
commitHash = await git.commit({
|
||||
fs,
|
||||
dir: appPath,
|
||||
commitHash = await gitCommit({
|
||||
path: appPath,
|
||||
message: message + " + extra files edited outside of Dyad",
|
||||
author: await getGitAuthor(),
|
||||
amend: true,
|
||||
});
|
||||
logger.log(
|
||||
|
||||
140
src/ipc/utils/git_utils.ts
Normal file
140
src/ipc/utils/git_utils.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { getGitAuthor } from "./git_author";
|
||||
import git from "isomorphic-git";
|
||||
import fs from "node:fs";
|
||||
import { promises as fsPromises } from "node:fs";
|
||||
import pathModule from "node:path";
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { readSettings } from "../../main/settings";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export async function gitCommit({
|
||||
path,
|
||||
message,
|
||||
amend,
|
||||
}: {
|
||||
path: string;
|
||||
message: string;
|
||||
amend?: boolean;
|
||||
}): Promise<string> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
let command = `git -C "${path}" commit -m "${message.replace(/"/g, '\\"')}"`;
|
||||
if (amend) {
|
||||
command += " --amend";
|
||||
}
|
||||
await execAsync(command);
|
||||
const { stdout } = await execAsync(`git -C "${path}" rev-parse HEAD`);
|
||||
return stdout.trim();
|
||||
} else {
|
||||
return git.commit({
|
||||
fs: fs,
|
||||
dir: path,
|
||||
message,
|
||||
author: await getGitAuthor(),
|
||||
amend: amend,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitCheckout({
|
||||
path,
|
||||
ref,
|
||||
}: {
|
||||
path: string;
|
||||
ref: string;
|
||||
}): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
await execAsync(`git -C "${path}" checkout "${ref.replace(/"/g, '\\"')}"`);
|
||||
return;
|
||||
} else {
|
||||
return git.checkout({ fs, dir: path, ref });
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitStageToRevert({
|
||||
path,
|
||||
targetOid,
|
||||
}: {
|
||||
path: string;
|
||||
targetOid: string;
|
||||
}): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
// Get the current HEAD commit hash
|
||||
const { stdout: currentHead } = await execAsync(
|
||||
`git -C "${path}" rev-parse HEAD`,
|
||||
);
|
||||
const currentCommit = currentHead.trim();
|
||||
|
||||
// If we're already at the target commit, nothing to do
|
||||
if (currentCommit === targetOid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Safety: refuse to run if the work-tree isn’t clean.
|
||||
const { stdout: wtStatus } = await execAsync(
|
||||
`git -C "${path}" status --porcelain`,
|
||||
);
|
||||
if (wtStatus.trim() !== "") {
|
||||
throw new Error("Cannot revert: working tree has uncommitted changes.");
|
||||
}
|
||||
|
||||
// Reset the working directory and index to match the target commit state
|
||||
// This effectively undoes all changes since the target commit
|
||||
await execAsync(`git -C "${path}" reset --hard "${targetOid}"`);
|
||||
|
||||
// Reset back to the original HEAD but keep the working directory as it is
|
||||
// This stages all the changes needed to revert to the target state
|
||||
await execAsync(`git -C "${path}" reset --soft "${currentCommit}"`);
|
||||
} else {
|
||||
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
|
||||
const matrix = await git.statusMatrix({
|
||||
fs,
|
||||
dir: path,
|
||||
ref: targetOid,
|
||||
});
|
||||
|
||||
// Process each file to revert to the state in previousVersionId
|
||||
for (const [filepath, headStatus, workdirStatus] of matrix) {
|
||||
const fullPath = pathModule.join(path, 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: path,
|
||||
oid: targetOid,
|
||||
filepath,
|
||||
});
|
||||
await fsPromises.mkdir(pathModule.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: path,
|
||||
filepath: filepath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stage all changes
|
||||
await git.add({
|
||||
fs,
|
||||
dir: path,
|
||||
filepath: ".",
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user