Unverified 提交 d3f3ac3a authored 作者: Adeniji Adekunle James's avatar Adeniji Adekunle James 提交者: GitHub

Replace native Git with Dugite to support users without Git installed (#1760)

I moved all isomorphic-git usage into a single git_utils.ts file and added Dugite as an alternative Git provider. The app now checks the user’s settings and uses dugite when user enabled native git for all isomorphic-git commands. This makes it easy to fully remove isomorphic-git in the future by updating only git_utils.ts. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds Dugite-based native Git (bundled binary) and refactors all Git calls to a unified git_utils API, replacing direct isomorphic-git usage across the app. > > - **Git Platform Abstraction**: > - Introduces `dugite` and bundles Git via Electron Forge (`extraResource`) with `LOCAL_GIT_DIRECTORY` setup in `src/main.ts`. > - Adds `src/ipc/git_types.ts` and a comprehensive `src/ipc/utils/git_utils.ts` wrapper supporting both Dugite (native) and `isomorphic-git` (fallback): `commit`, `add`/`addAll`, `remove`, `init`, `clone`, `push`, `setRemoteUrl`, `currentBranch`, `listBranches`, `renameBranch`, `log`, `isIgnored`, `getCurrentCommitHash`, `getGitUncommittedFiles`, `getFileAtCommit`, `checkout`, `stageToRevert`. > - **Refactors (switch to git_utils)**: > - Replaces direct `isomorphic-git` imports in handlers and processors: `app_handlers`, `chat_handlers`, `createFromTemplate`, `github_handlers`, `import_handlers`, `portal_handlers`, `version_handlers`, `response_processor`, `neon_timestamp_utils`, `utils/codebase`. > - Updates tests to mock `git_utils` (`src/__tests__/chat_stream_handlers.test.ts`). > - **Behavioral/Feature Updates**: > - `createFromTemplate` uses `fetch` for GitHub API and `gitClone` for cloning with cache validation. > - GitHub integration uses `gitSetRemoteUrl`/`gitPush`/`gitClone`, handling public vs token URLs and directory creation when native Git is disabled. > - Versioning, imports, app file edits, migrations now stage/commit via `git_utils`. > - **UI/Copy**: > - Updates Settings description for “Enable Native Git”. > - **Config/Version**: > - Bumps version to `0.29.0-beta.1`; adds `dugite` dependency. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ba098f7f25d85fc6330a41dc718fbfd43fff2d6c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: 's avatarWill Chen <willchen90@gmail.com>
上级 a7bcec22
......@@ -74,6 +74,7 @@ const config: ForgeConfig = {
},
asar: true,
ignore,
extraResource: ["node_modules/dugite/git"],
// ignore: [/node_modules\/(?!(better-sqlite3|bindings|file-uri-to-path)\/)/],
},
rebuildConfig: {
......
{
"name": "dyad",
"version": "0.28.0",
"version": "0.29.0-beta.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dyad",
"version": "0.28.0",
"version": "0.29.0-beta.1",
"license": "MIT",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^3.0.15",
......@@ -59,6 +59,7 @@
"date-fns": "^4.1.0",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.41.0",
"dugite": "^3.0.0",
"electron-log": "^5.3.3",
"electron-playwright-helpers": "^1.7.1",
"electron-squirrel-startup": "^1.0.1",
......@@ -8090,6 +8091,20 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b4a": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
......@@ -8106,6 +8121,20 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
......@@ -10203,6 +10232,31 @@
}
}
},
"node_modules/dugite": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/dugite/-/dugite-3.0.0.tgz",
"integrity": "sha512-+q2i3y5TvlC2YaZofkdELHtmvHbT6yuBODimItxU6xEGtHqRt6rpApJzf6lAqtpo+y1gokhfsHyULH0yNZuTWQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"progress": "^2.0.3",
"tar-stream": "^3.1.7"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/dugite/node_modules/tar-stream": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
......@@ -11584,6 +11638,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
......@@ -11808,6 +11871,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
......@@ -17970,7 +18039,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
......@@ -19985,6 +20053,17 @@
"node": ">= 0.4"
}
},
"node_modules/streamx": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
......@@ -20524,6 +20603,15 @@
"rimraf": "bin.js"
}
},
"node_modules/text-decoder": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
......
......@@ -135,6 +135,7 @@
"date-fns": "^4.1.0",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.41.0",
"dugite": "^3.0.0",
"electron-log": "^5.3.3",
"electron-playwright-helpers": "^1.7.1",
"electron-squirrel-startup": "^1.0.1",
......
......@@ -13,9 +13,9 @@ import {
hasUnclosedDyadWrite,
} from "../ipc/handlers/chat_stream_handlers";
import fs from "node:fs";
import git from "isomorphic-git";
import { db } from "../db";
import { cleanFullResponse } from "@/ipc/utils/cleanFullResponse";
import { cleanFullResponse } from "../ipc/utils/cleanFullResponse";
import { gitAdd, gitRemove, gitCommit } from "../ipc/utils/git_utils";
// Mock fs with default export
vi.mock("node:fs", async () => {
......@@ -43,14 +43,19 @@ vi.mock("node:fs", async () => {
};
});
// Mock isomorphic-git
vi.mock("isomorphic-git", () => ({
default: {
add: vi.fn().mockResolvedValue(undefined),
remove: vi.fn().mockResolvedValue(undefined),
commit: vi.fn().mockResolvedValue(undefined),
statusMatrix: vi.fn().mockResolvedValue([]),
},
// Mock Git utils
vi.mock("../ipc/utils/git_utils", () => ({
gitAdd: vi.fn(),
gitCommit: vi.fn(),
gitRemove: vi.fn(),
gitRenameBranch: vi.fn(),
gitCurrentBranch: vi.fn(),
gitLog: vi.fn(),
gitInit: vi.fn(),
gitPush: vi.fn(),
gitSetRemoteUrl: vi.fn(),
gitStatus: vi.fn().mockResolvedValue([]),
getGitUncommittedFiles: vi.fn().mockResolvedValue([]),
}));
// Mock paths module to control getDyadAppPath
......@@ -703,12 +708,12 @@ describe("processFullResponse", () => {
"/mock/user/data/path/mock-app-path/src/file1.js",
"console.log('Hello');",
);
expect(git.add).toHaveBeenCalledWith(
expect(gitAdd).toHaveBeenCalledWith(
expect.objectContaining({
filepath: "src/file1.js",
}),
);
expect(git.commit).toHaveBeenCalled();
expect(gitCommit).toHaveBeenCalled();
expect(result).toEqual({ updatedFiles: true });
});
......@@ -783,24 +788,24 @@ describe("processFullResponse", () => {
);
// Verify git operations were called for each file
expect(git.add).toHaveBeenCalledWith(
expect(gitAdd).toHaveBeenCalledWith(
expect.objectContaining({
filepath: "src/file1.js",
}),
);
expect(git.add).toHaveBeenCalledWith(
expect(gitAdd).toHaveBeenCalledWith(
expect.objectContaining({
filepath: "src/utils/file2.js",
}),
);
expect(git.add).toHaveBeenCalledWith(
expect(gitAdd).toHaveBeenCalledWith(
expect.objectContaining({
filepath: "src/components/Button.tsx",
}),
);
// Verify commit was called once after all files were added
expect(git.commit).toHaveBeenCalledTimes(1);
expect(gitCommit).toHaveBeenCalledTimes(1);
expect(result).toEqual({ updatedFiles: true });
});
......@@ -825,17 +830,17 @@ describe("processFullResponse", () => {
"/mock/user/data/path/mock-app-path/src/components/OldComponent.jsx",
"/mock/user/data/path/mock-app-path/src/components/NewComponent.jsx",
);
expect(git.add).toHaveBeenCalledWith(
expect(gitAdd).toHaveBeenCalledWith(
expect.objectContaining({
filepath: "src/components/NewComponent.jsx",
}),
);
expect(git.remove).toHaveBeenCalledWith(
expect(gitRemove).toHaveBeenCalledWith(
expect.objectContaining({
filepath: "src/components/OldComponent.jsx",
}),
);
expect(git.commit).toHaveBeenCalled();
expect(gitCommit).toHaveBeenCalled();
expect(result).toEqual({ updatedFiles: true });
});
......@@ -852,7 +857,7 @@ describe("processFullResponse", () => {
expect(fs.mkdirSync).toHaveBeenCalled();
expect(fs.renameSync).not.toHaveBeenCalled();
expect(git.commit).not.toHaveBeenCalled();
expect(gitCommit).not.toHaveBeenCalled();
expect(result).toEqual({
updatedFiles: false,
extraFiles: undefined,
......@@ -875,12 +880,12 @@ describe("processFullResponse", () => {
expect(fs.unlinkSync).toHaveBeenCalledWith(
"/mock/user/data/path/mock-app-path/src/components/Unused.jsx",
);
expect(git.remove).toHaveBeenCalledWith(
expect(gitRemove).toHaveBeenCalledWith(
expect.objectContaining({
filepath: "src/components/Unused.jsx",
}),
);
expect(git.commit).toHaveBeenCalled();
expect(gitCommit).toHaveBeenCalled();
expect(result).toEqual({ updatedFiles: true });
});
......@@ -896,8 +901,8 @@ describe("processFullResponse", () => {
});
expect(fs.unlinkSync).not.toHaveBeenCalled();
expect(git.remove).not.toHaveBeenCalled();
expect(git.commit).not.toHaveBeenCalled();
expect(gitRemove).not.toHaveBeenCalled();
expect(gitCommit).not.toHaveBeenCalled();
expect(result).toEqual({
updatedFiles: false,
extraFiles: undefined,
......@@ -942,11 +947,11 @@ describe("processFullResponse", () => {
);
// Check git operations
expect(git.add).toHaveBeenCalledTimes(2); // For the write and rename
expect(git.remove).toHaveBeenCalledTimes(2); // For the rename and delete
expect(gitAdd).toHaveBeenCalledTimes(2); // For the write and rename
expect(gitRemove).toHaveBeenCalledTimes(2); // For the rename and delete
// Check the commit message includes all operations
expect(git.commit).toHaveBeenCalledWith(
expect(gitCommit).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining(
"wrote 1 file(s), renamed 1 file(s), deleted 1 file(s)",
......
// Type definitions for Git operations
export type GitCommit = {
oid: string;
commit: {
message: string;
author: {
timestamp: number;
};
};
};
export interface GitBaseParams {
path: string;
}
export interface GitCommitParams extends GitBaseParams {
message: string;
amend?: boolean;
}
export interface GitFileParams extends GitBaseParams {
filepath: string;
}
export interface GitCheckoutParams extends GitBaseParams {
ref: string;
}
export interface GitBranchRenameParams extends GitBaseParams {
oldBranch: string;
newBranch: string;
}
export interface GitCloneParams {
path: string; // destination
url: string;
depth?: number | null;
singleBranch?: boolean;
accessToken?: string;
}
export interface GitLogParams extends GitBaseParams {
depth?: number;
}
export interface GitResult {
success: boolean;
error?: string;
}
export interface GitPushParams extends GitBaseParams {
branch: string;
accessToken: string;
force?: boolean;
}
export interface GitFileAtCommitParams extends GitBaseParams {
filePath: string;
commitHash: string;
}
export interface GitSetRemoteUrlParams extends GitBaseParams {
remoteUrl: string;
}
export interface GitInitParams extends GitBaseParams {
ref?: string; // branch name, default = "main"
}
export interface GitStageToRevertParams extends GitBaseParams {
targetOid: string;
}
......@@ -14,7 +14,6 @@ import fs from "node:fs";
import path from "node:path";
import { getDyadAppPath, getUserDataPath } from "../../paths/paths";
import { ChildProcess, spawn } from "node:child_process";
import git from "isomorphic-git";
import { promises as fsPromises } from "node:fs";
// Import our utility modules
......@@ -44,7 +43,13 @@ 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";
import {
gitCommit,
gitAdd,
gitInit,
gitListBranches,
gitRenameBranch,
} from "../utils/git_utils";
import { safeSend } from "../utils/safe_sender";
import { normalizePath } from "../../../shared/normalizePath";
import { isServerFunction } from "@/supabase_admin/supabase_utils";
......@@ -585,18 +590,11 @@ export function registerAppHandlers() {
});
// Initialize git repo and create first commit
await git.init({
fs: fs,
dir: fullAppPath,
defaultBranch: "main",
});
await gitInit({ path: fullAppPath, ref: "main" });
// Stage all files
await git.add({
fs: fs,
dir: fullAppPath,
filepath: ".",
});
await gitAdd({ path: fullAppPath, filepath: "." });
// Create initial commit
const commitHash = await gitCommit({
......@@ -657,18 +655,10 @@ export function registerAppHandlers() {
if (!withHistory) {
// Initialize git repo and create first commit
await git.init({
fs: fs,
dir: newAppPath,
defaultBranch: "main",
});
await gitInit({ path: newAppPath, ref: "main" });
// Stage all files
await git.add({
fs: fs,
dir: newAppPath,
filepath: ".",
});
await gitAdd({ path: newAppPath, filepath: "." });
// Create initial commit
await gitCommit({
......@@ -1049,11 +1039,7 @@ export function registerAppHandlers() {
// Check if git repository exists and commit the change
if (fs.existsSync(path.join(appPath, ".git"))) {
await git.add({
fs,
dir: appPath,
filepath: filePath,
});
await gitAdd({ path: appPath, filepath: filePath });
await gitCommit({
path: appPath,
......@@ -1398,7 +1384,7 @@ export function registerAppHandlers() {
return withLock(appId, async () => {
try {
// Check if the old branch exists
const branches = await git.listBranches({ fs, dir: appPath });
const branches = await gitListBranches({ path: appPath });
if (!branches.includes(oldBranchName)) {
throw new Error(`Branch '${oldBranchName}' not found.`);
}
......@@ -1414,11 +1400,10 @@ export function registerAppHandlers() {
);
}
await git.renameBranch({
fs: fs,
dir: appPath,
oldref: oldBranchName,
ref: newBranchName,
await gitRenameBranch({
path: appPath,
oldBranch: oldBranchName,
newBranch: newBranchName,
});
logger.info(
`Branch renamed from '${oldBranchName}' to '${newBranchName}' for app ${appId}`,
......
......@@ -3,13 +3,12 @@ import { db } from "../../db";
import { apps, chats, messages } from "../../db/schema";
import { desc, eq, and, like } from "drizzle-orm";
import type { ChatSearchResult, ChatSummary } from "../../lib/schemas";
import * as git from "isomorphic-git";
import * as fs from "fs";
import { createLoggedHandler } from "./safe_handle";
import log from "electron-log";
import { getDyadAppPath } from "../../paths/paths";
import { UpdateChatParams } from "../ipc_types";
import { getCurrentCommitHash } from "../utils/git_utils";
const logger = log.scope("chat_handlers");
const handle = createLoggedHandler(logger);
......@@ -31,9 +30,8 @@ export function registerChatHandlers() {
let initialCommitHash = null;
try {
// Get the current git revision of main branch
initialCommitHash = await git.resolveRef({
fs,
dir: getDyadAppPath(app.path),
initialCommitHash = await getCurrentCommitHash({
path: getDyadAppPath(app.path),
ref: "main",
});
} catch (error) {
......
import path from "path";
import fs from "fs-extra";
import git from "isomorphic-git";
import http from "isomorphic-git/http/node";
import { app } from "electron";
import { copyDirectoryRecursive } from "../utils/file_utils";
import { gitClone, getCurrentCommitHash } from "../utils/git_utils";
import { readSettings } from "@/main/settings";
import { getTemplateOrThrow } from "../utils/template_utils";
import log from "electron-log";
......@@ -35,9 +34,6 @@ export async function createFromTemplate({
}
async function cloneRepo(repoUrl: string): Promise<string> {
let orgName: string;
let repoName: string;
const url = new URL(repoUrl);
if (url.protocol !== "https:") {
throw new Error("Repository URL must use HTTPS.");
......@@ -55,8 +51,8 @@ async function cloneRepo(repoUrl: string): Promise<string> {
);
}
orgName = pathParts[0];
repoName = path.basename(pathParts[1], ".git"); // Remove .git suffix if present
const orgName = pathParts[0];
const repoName = path.basename(pathParts[1], ".git"); // Remove .git suffix if present
if (!orgName || !repoName) {
// This case should ideally be caught by pathParts.length !== 2
......@@ -83,41 +79,31 @@ async function cloneRepo(repoUrl: string): Promise<string> {
const apiUrl = `https://api.github.com/repos/${orgName}/${repoName}/commits/HEAD`;
logger.info(`Fetching remote SHA from ${apiUrl}`);
let remoteSha: string | undefined;
const response = await http.request({
url: apiUrl,
// Use native fetch instead of isomorphic-git http.request
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"User-Agent": "Dyad", // GitHub API requires a User-Agent
"User-Agent": "Dyad", // GitHub API requires this
Accept: "application/vnd.github.v3+json",
},
});
if (response.statusCode === 200 && response.body) {
// Convert AsyncIterableIterator<Uint8Array> to string
const chunks: Uint8Array[] = [];
for await (const chunk of response.body) {
chunks.push(chunk);
}
const responseBodyStr = Buffer.concat(chunks).toString("utf8");
const commitData = JSON.parse(responseBodyStr);
remoteSha = commitData.sha;
if (!remoteSha) {
throw new Error("SHA not found in GitHub API response.");
}
logger.info(`Successfully fetched remote SHA: ${remoteSha}`);
} else {
// Handle non-200 responses
if (!response.ok) {
throw new Error(
`GitHub API request failed with status ${response.statusCode}: ${response.statusMessage}`,
`GitHub API request failed with status ${response.status}: ${response.statusText}`,
);
}
// Parse JSON directly (fetch handles streaming internally)
const commitData = await response.json();
const remoteSha = commitData.sha;
if (!remoteSha) {
throw new Error("SHA not found in GitHub API response.");
}
const localSha = await git.resolveRef({
fs,
dir: cachePath,
ref: "HEAD",
});
logger.info(`Successfully fetched remote SHA: ${remoteSha}`);
// Compare with local SHA
const localSha = await getCurrentCommitHash({ path: cachePath });
if (remoteSha === localSha) {
logger.info(
......@@ -129,7 +115,7 @@ async function cloneRepo(repoUrl: string): Promise<string> {
`Local cache for ${repoName} (SHA: ${localSha}) is outdated (Remote SHA: ${remoteSha}). Removing and re-cloning.`,
);
fs.rmSync(cachePath, { recursive: true, force: true });
// Proceed to clone
// Continue to clone…
}
} catch (err) {
logger.warn(
......@@ -144,14 +130,7 @@ async function cloneRepo(repoUrl: string): Promise<string> {
logger.info(`Cloning ${repoUrl} to ${cachePath}`);
try {
await git.clone({
fs,
http,
dir: cachePath,
url: repoUrl,
singleBranch: true,
depth: 1,
});
await gitClone({ path: cachePath, url: repoUrl, depth: 1 });
logger.info(`Successfully cloned ${repoUrl} to ${cachePath}`);
} catch (err) {
logger.error(`Failed to clone ${repoUrl} to ${cachePath}: `, err);
......
import { ipcMain, BrowserWindow, IpcMainInvokeEvent } from "electron";
import fetch from "node-fetch"; // Use node-fetch for making HTTP requests in main process
import { writeSettings, readSettings } from "../../main/settings";
import git, { clone } from "isomorphic-git";
import http from "isomorphic-git/http/node";
import { gitSetRemoteUrl, gitPush, gitClone } from "../utils/git_utils";
import * as schema from "../../db/schema";
import fs from "node:fs";
import { getDyadAppPath } from "../../paths/paths";
......@@ -575,25 +574,17 @@ async function handlePushToGithub(
? `${GITHUB_GIT_BASE}/${app.githubOrg}/${app.githubRepo}.git`
: `https://${accessToken}:x-oauth-basic@github.com/${app.githubOrg}/${app.githubRepo}.git`;
// Set or update remote URL using git config
await git.setConfig({
fs,
dir: appPath,
path: "remote.origin.url",
value: remoteUrl,
await gitSetRemoteUrl({
path: appPath,
remoteUrl,
});
// Push to GitHub
await git.push({
fs,
http,
dir: appPath,
remote: "origin",
ref: "main",
remoteRef: branch,
onAuth: () => ({
username: accessToken,
password: "x-oauth-basic",
}),
force: !!force,
await gitPush({
path: appPath,
branch,
accessToken,
force,
});
return { success: true };
} catch (err: any) {
......@@ -673,8 +664,11 @@ async function handleCloneRepoFromUrl(
}
const appPath = getDyadAppPath(finalAppName);
if (!fs.existsSync(appPath)) {
fs.mkdirSync(appPath, { recursive: true });
// Ensure the app directory exists if native git is disabled
if (!settings.enableNativeGit) {
if (!fs.existsSync(appPath)) {
fs.mkdirSync(appPath, { recursive: true });
}
}
// Use authenticated URL if token exists, otherwise use public HTTPS URL
const cloneUrl = accessToken
......@@ -683,17 +677,10 @@ async function handleCloneRepoFromUrl(
: `https://${accessToken}:x-oauth-basic@github.com/${owner}/${repoName}.git`
: `https://github.com/${owner}/${repoName}.git`; // Changed: use public HTTPS URL instead of original url
try {
await clone({
fs,
http,
dir: appPath,
await gitClone({
path: appPath,
url: cloneUrl,
onAuth: accessToken
? () => ({
username: accessToken,
password: "x-oauth-basic",
})
: undefined,
accessToken,
singleBranch: false,
});
} catch (cloneErr) {
......
......@@ -8,11 +8,10 @@ import { apps } from "@/db/schema";
import { db } from "@/db";
import { chats } from "@/db/schema";
import { eq } from "drizzle-orm";
import git from "isomorphic-git";
import { ImportAppParams, ImportAppResult } from "../ipc_types";
import { copyDirectoryRecursive } from "../utils/file_utils";
import { gitCommit } from "../utils/git_utils";
import { gitCommit, gitAdd, gitInit } from "../utils/git_utils";
const logger = log.scope("import-handlers");
const handle = createLoggedHandler(logger);
......@@ -106,18 +105,11 @@ export function registerImportHandlers() {
.catch(() => false);
if (!isGitRepo) {
// Initialize git repo and create first commit
await git.init({
fs: fs,
dir: destPath,
defaultBranch: "main",
});
await gitInit({ path: destPath, ref: "main" });
// Stage all files
await git.add({
fs: fs,
dir: destPath,
filepath: ".",
});
await gitAdd({ path: destPath, filepath: "." });
// Create initial commit
await gitCommit({
......
......@@ -5,9 +5,7 @@ import { apps } from "../../db/schema";
import { eq } from "drizzle-orm";
import { getDyadAppPath } from "../../paths/paths";
import { spawn } from "child_process";
import fs from "node:fs";
import git from "isomorphic-git";
import { gitCommit } from "../utils/git_utils";
import { gitCommit, gitAdd } from "../utils/git_utils";
import { storeDbTimestampAtCurrentVersion } from "../utils/neon_timestamp_utils";
const logger = log.scope("portal_handlers");
......@@ -116,11 +114,7 @@ export function registerPortalHandlers() {
// Stage all changes and commit
try {
await git.add({
fs,
dir: appPath,
filepath: ".",
});
await gitAdd({ path: appPath, filepath: "." });
const commitHash = await gitCommit({
path: appPath,
......
......@@ -7,15 +7,23 @@ import type {
RevertVersionParams,
RevertVersionResponse,
} from "../ipc_types";
import type { GitCommit } from "../git_types";
import fs from "node:fs";
import path from "node:path";
import { getDyadAppPath } from "../../paths/paths";
import git, { type ReadCommitResult } from "isomorphic-git";
import { withLock } from "../utils/lock_utils";
import log from "electron-log";
import { createLoggedHandler } from "./safe_handle";
import { gitCheckout, gitCommit, gitStageToRevert } from "../utils/git_utils";
import { deployAllSupabaseFunctions } from "../../supabase_admin/supabase_utils";
import {
gitCheckout,
gitCommit,
gitStageToRevert,
getCurrentCommitHash,
gitCurrentBranch,
gitLog,
} from "../utils/git_utils";
import {
getNeonClient,
......@@ -80,11 +88,9 @@ export function registerVersionHandlers() {
return [];
}
const commits = await git.log({
fs,
dir: appPath,
// KEEP UP TO DATE WITH ChatHeader.tsx
depth: 100_000, // Limit to last 100_000 commits for performance
const commits = await gitLog({
path: appPath,
depth: 100_000, // KEEP UP TO DATE WITH ChatHeader.tsx
});
// Get all snapshots for this app to match with commits
......@@ -104,7 +110,7 @@ export function registerVersionHandlers() {
});
}
return commits.map((commit: ReadCommitResult) => {
return commits.map((commit: GitCommit) => {
const snapshotInfo = snapshotMap.get(commit.oid);
return {
oid: commit.oid,
......@@ -134,11 +140,7 @@ export function registerVersionHandlers() {
}
try {
const currentBranch = await git.currentBranch({
fs,
dir: appPath,
fullname: false,
});
const currentBranch = await gitCurrentBranch({ path: appPath });
return {
branch: currentBranch || "<no-branch>",
......@@ -169,9 +171,8 @@ export function registerVersionHandlers() {
const appPath = getDyadAppPath(app.path);
// Get the current commit hash before reverting
const currentCommitHash = await git.resolveRef({
fs,
dir: appPath,
const currentCommitHash = await getCurrentCommitHash({
path: appPath,
ref: "main",
});
......
......@@ -518,6 +518,7 @@ export interface GithubRepository {
full_name: string;
private: boolean;
}
export type CloneRepoReturnType =
| {
app: App;
......
......@@ -4,7 +4,6 @@ import { and, eq } from "drizzle-orm";
import fs from "node:fs";
import { getDyadAppPath } from "../../paths/paths";
import path from "node:path";
import git from "isomorphic-git";
import { safeJoin } from "../utils/path_utils";
import log from "electron-log";
......@@ -16,7 +15,13 @@ import {
} from "../../supabase_admin/supabase_management_client";
import { isServerFunction } from "../../supabase_admin/supabase_utils";
import { UserSettings } from "../../lib/schemas";
import { gitCommit } from "../utils/git_utils";
import {
gitCommit,
gitAdd,
gitRemove,
gitAddAll,
getGitUncommittedFiles,
} from "../utils/git_utils";
import { readSettings } from "@/main/settings";
import { writeMigrationFile } from "../utils/file_utils";
import {
......@@ -265,11 +270,7 @@ export async function processFullResponseActions(
// Remove the file from git
try {
await git.remove({
fs,
dir: appPath,
filepath: filePath,
});
await gitRemove({ path: appPath, filepath: filePath });
} catch (error) {
logger.warn(`Failed to git remove deleted file ${filePath}:`, error);
// Continue even if remove fails as the file was still deleted
......@@ -308,17 +309,9 @@ export async function processFullResponseActions(
renamedFiles.push(tag.to);
// Add the new file and remove the old one from git
await git.add({
fs,
dir: appPath,
filepath: tag.to,
});
await gitAdd({ path: appPath, filepath: tag.to });
try {
await git.remove({
fs,
dir: appPath,
filepath: tag.from,
});
await gitRemove({ path: appPath, filepath: tag.from });
} catch (error) {
logger.warn(`Failed to git remove old file ${tag.from}:`, error);
// Continue even if remove fails as the file was still renamed
......@@ -469,11 +462,7 @@ export async function processFullResponseActions(
if (hasChanges) {
// Stage all written files
for (const file of writtenFiles) {
await git.add({
fs,
dir: appPath,
filepath: file,
});
await gitAdd({ path: appPath, filepath: file });
}
// Create commit with details of all changes
......@@ -502,18 +491,11 @@ export async function processFullResponseActions(
logger.log(`Successfully committed changes: ${changes.join(", ")}`);
// Check for any uncommitted changes after the commit
const statusMatrix = await git.statusMatrix({ fs, dir: appPath });
uncommittedFiles = statusMatrix
.filter((row) => row[1] !== 1 || row[2] !== 1 || row[3] !== 1)
.map((row) => row[0]); // Get just the file paths
uncommittedFiles = await getGitUncommittedFiles({ path: appPath });
if (uncommittedFiles.length > 0) {
// Stage all changes
await git.add({
fs,
dir: appPath,
filepath: ".",
});
await gitAddAll({ path: appPath });
try {
commitHash = await gitCommit({
path: appPath,
......
差异被折叠。
import { db } from "../../db";
import { versions, apps } from "../../db/schema";
import { eq, and } from "drizzle-orm";
import fs from "node:fs";
import git from "isomorphic-git";
import { getDyadAppPath } from "../../paths/paths";
import { neon } from "@neondatabase/serverless";
import log from "electron-log";
import { getNeonClient } from "@/neon_admin/neon_management_client";
import { getCurrentCommitHash } from "./git_utils";
const logger = log.scope("neon_timestamp_utils");
......@@ -62,11 +61,7 @@ export async function storeDbTimestampAtCurrentVersion({
// 2. Get the current commit hash
const appPath = getDyadAppPath(app.path);
const currentCommitHash = await git.resolveRef({
fs,
dir: appPath,
ref: "HEAD",
});
const currentCommitHash = await getCurrentCommitHash({ path: appPath });
logger.info(`Current commit hash: ${currentCommitHash}`);
......
......@@ -24,6 +24,7 @@ import {
AddPromptDataSchema,
AddPromptPayload,
} from "./ipc/deep_link_data";
import fs from "fs";
log.errorHandler.startCatching();
log.eventLogger.startLogging();
......@@ -42,6 +43,22 @@ if (started) {
app.quit();
}
// Decide the git directory depending on environment
function resolveLocalGitDirectory() {
if (!app.isPackaged) {
// Dev: app.getAppPath() is the project root
return path.join(app.getAppPath(), "node_modules/dugite/git");
}
// Packaged app: git is bundled via extraResource
return path.join(process.resourcesPath, "git");
}
const gitDir = resolveLocalGitDirectory();
if (fs.existsSync(gitDir)) {
process.env.LOCAL_GIT_DIRECTORY = gitDir;
}
// https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#main-process-mainjs
if (process.defaultApp) {
if (process.argv.length >= 2) {
......
......@@ -163,18 +163,8 @@ export default function SettingsPage() {
<Label htmlFor="enable-native-git">Enable Native Git</Label>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Native Git offers faster performance but requires{" "}
<a
onClick={() => {
IpcClient.getInstance().openExternalUrl(
"https://git-scm.com/downloads",
);
}}
className="text-blue-600 hover:underline dark:text-blue-400"
>
installing Git
</a>
.
This doesn't require any external Git installation and offers
a faster, native-Git performance experience.
</div>
</div>
</div>
......
import fs from "node:fs";
import fsAsync from "node:fs/promises";
import path from "node:path";
import { isIgnored } from "isomorphic-git";
import { gitIsIgnored } from "../ipc/utils/git_utils";
import log from "electron-log";
import { IS_TEST_BUILD } from "../ipc/utils/test_utils";
import { glob } from "glob";
......@@ -176,9 +175,8 @@ async function isGitIgnored(
}
const relativePath = path.relative(baseDir, filePath);
const result = await isIgnored({
fs,
dir: baseDir,
const result = await gitIsIgnored({
path: baseDir,
filepath: relativePath,
});
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论