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: Will Chen <willchen90@gmail.com>
This commit is contained in:
committed by
GitHub
parent
a7bcec220a
commit
d3f3ac3ae1
@@ -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: {
|
||||
|
||||
94
package-lock.json
generated
94
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"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)",
|
||||
|
||||
60
src/ipc/git_types.ts
Normal file
60
src/ipc/git_types.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
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);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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,
|
||||
|
||||
@@ -1,42 +1,67 @@
|
||||
import { getGitAuthor } from "./git_author";
|
||||
import git from "isomorphic-git";
|
||||
import http from "isomorphic-git/http/node";
|
||||
import { exec } from "dugite";
|
||||
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";
|
||||
import log from "electron-log";
|
||||
const logger = log.scope("git_utils");
|
||||
const execAsync = promisify(exec);
|
||||
import type {
|
||||
GitBaseParams,
|
||||
GitFileParams,
|
||||
GitCheckoutParams,
|
||||
GitBranchRenameParams,
|
||||
GitCloneParams,
|
||||
GitCommitParams,
|
||||
GitLogParams,
|
||||
GitFileAtCommitParams,
|
||||
GitSetRemoteUrlParams,
|
||||
GitStageToRevertParams,
|
||||
GitInitParams,
|
||||
GitPushParams,
|
||||
GitCommit,
|
||||
} from "../git_types";
|
||||
|
||||
async function verboseExecAsync(
|
||||
command: string,
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
try {
|
||||
return await execAsync(command);
|
||||
} catch (error: any) {
|
||||
let errorMessage = `Error: ${error.message}`;
|
||||
if (error.stdout) {
|
||||
errorMessage += `\nStdout: ${error.stdout}`;
|
||||
}
|
||||
if (error.stderr) {
|
||||
errorMessage += `\nStderr: ${error.stderr}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
/**
|
||||
* Helper function that wraps exec and throws an error if the exit code is non-zero
|
||||
*/
|
||||
async function execOrThrow(
|
||||
args: string[],
|
||||
path: string,
|
||||
errorMessage?: string,
|
||||
): Promise<void> {
|
||||
const result = await exec(args, path);
|
||||
if (result.exitCode !== 0) {
|
||||
const errorDetails = result.stderr.trim() || result.stdout.trim();
|
||||
const error = errorMessage
|
||||
? `${errorMessage}. ${errorDetails}`
|
||||
: `Git command failed: ${args.join(" ")}. ${errorDetails}`;
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCurrentCommitHash({
|
||||
path,
|
||||
}: {
|
||||
path: string;
|
||||
}): Promise<string> {
|
||||
return await git.resolveRef({
|
||||
fs,
|
||||
dir: path,
|
||||
ref: "HEAD",
|
||||
});
|
||||
ref = "HEAD",
|
||||
}: GitInitParams): Promise<string> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
const result = await exec(["rev-parse", ref], path);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to resolve ref '${ref}': ${result.stderr.trim() || result.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
return result.stdout.trim();
|
||||
} else {
|
||||
return await git.resolveRef({
|
||||
fs,
|
||||
dir: path,
|
||||
ref,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function isGitStatusClean({
|
||||
@@ -46,8 +71,15 @@ export async function isGitStatusClean({
|
||||
}): Promise<boolean> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
const { stdout } = await execAsync(`git -C "${path}" status --porcelain`);
|
||||
return stdout.trim() === "";
|
||||
const result = await exec(["status", "--porcelain"], path);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Failed to get status: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// If output is empty, working directory is clean (no changes)
|
||||
const isClean = result.stdout.trim().length === 0;
|
||||
return isClean;
|
||||
} else {
|
||||
const statusMatrix = await git.statusMatrix({ fs, dir: path });
|
||||
return statusMatrix.every(
|
||||
@@ -60,21 +92,31 @@ export async function gitCommit({
|
||||
path,
|
||||
message,
|
||||
amend,
|
||||
}: {
|
||||
path: string;
|
||||
message: string;
|
||||
amend?: boolean;
|
||||
}): Promise<string> {
|
||||
}: GitCommitParams): Promise<string> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
let command = `git -C "${path}" commit -m "${message.replace(/"/g, '\\"')}"`;
|
||||
// Get author info to match isomorphic-git behavior
|
||||
const author = await getGitAuthor();
|
||||
// Perform the commit using dugite with --author flag
|
||||
const args = [
|
||||
"commit",
|
||||
"-m",
|
||||
message,
|
||||
"--author",
|
||||
`${author.name} <${author.email}>`,
|
||||
];
|
||||
if (amend) {
|
||||
command += " --amend";
|
||||
args.push("--amend");
|
||||
}
|
||||
|
||||
await verboseExecAsync(command);
|
||||
const { stdout } = await execAsync(`git -C "${path}" rev-parse HEAD`);
|
||||
return stdout.trim();
|
||||
await execOrThrow(args, path, "Failed to create commit");
|
||||
// Get the new commit hash
|
||||
const result = await exec(["rev-parse", "HEAD"], path);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get commit hash: ${result.stderr.trim() || result.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
return result.stdout.trim();
|
||||
} else {
|
||||
return git.commit({
|
||||
fs: fs,
|
||||
@@ -89,13 +131,14 @@ export async function gitCommit({
|
||||
export async function gitCheckout({
|
||||
path,
|
||||
ref,
|
||||
}: {
|
||||
path: string;
|
||||
ref: string;
|
||||
}): Promise<void> {
|
||||
}: GitCheckoutParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
await execAsync(`git -C "${path}" checkout "${ref.replace(/"/g, '\\"')}"`);
|
||||
await execOrThrow(
|
||||
["checkout", ref],
|
||||
path,
|
||||
`Failed to checkout ref '${ref}'`,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
return git.checkout({ fs, dir: path, ref });
|
||||
@@ -105,17 +148,18 @@ export async function gitCheckout({
|
||||
export async function gitStageToRevert({
|
||||
path,
|
||||
targetOid,
|
||||
}: {
|
||||
path: string;
|
||||
targetOid: string;
|
||||
}): Promise<void> {
|
||||
}: GitStageToRevertParams): 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();
|
||||
const currentHeadResult = await exec(["rev-parse", "HEAD"], path);
|
||||
if (currentHeadResult.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get current commit: ${currentHeadResult.stderr.trim() || currentHeadResult.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const currentCommit = currentHeadResult.stdout.trim();
|
||||
|
||||
// If we're already at the target commit, nothing to do
|
||||
if (currentCommit === targetOid) {
|
||||
@@ -123,20 +167,31 @@ export async function gitStageToRevert({
|
||||
}
|
||||
|
||||
// Safety: refuse to run if the work-tree isn't clean.
|
||||
const { stdout: wtStatus } = await execAsync(
|
||||
`git -C "${path}" status --porcelain`,
|
||||
);
|
||||
if (wtStatus.trim() !== "") {
|
||||
const statusResult = await exec(["status", "--porcelain"], path);
|
||||
if (statusResult.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get status: ${statusResult.stderr.trim() || statusResult.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
if (statusResult.stdout.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}"`);
|
||||
await execOrThrow(
|
||||
["reset", "--hard", targetOid],
|
||||
path,
|
||||
`Failed to reset to target commit '${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}"`);
|
||||
await execOrThrow(
|
||||
["reset", "--soft", currentCommit],
|
||||
path,
|
||||
"Failed to reset back to original HEAD",
|
||||
);
|
||||
} else {
|
||||
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
|
||||
const matrix = await git.statusMatrix({
|
||||
@@ -187,32 +242,111 @@ export async function gitStageToRevert({
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitAddAll({ path }: { path: string }): Promise<void> {
|
||||
export async function gitAddAll({ path }: GitBaseParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
await execAsync(`git -C "${path}" add .`);
|
||||
await execOrThrow(["add", "."], path, "Failed to stage all files");
|
||||
return;
|
||||
} else {
|
||||
return git.add({ fs, dir: path, filepath: "." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitAdd({ path, filepath }: GitFileParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
await execOrThrow(
|
||||
["add", "--", filepath],
|
||||
path,
|
||||
`Failed to stage file '${filepath}'`,
|
||||
);
|
||||
} else {
|
||||
await git.add({
|
||||
fs,
|
||||
dir: path,
|
||||
filepath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitInit({
|
||||
path,
|
||||
ref = "main",
|
||||
}: GitInitParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
await execOrThrow(
|
||||
["init", "-b", ref],
|
||||
path,
|
||||
`Failed to initialize git repository with branch '${ref}'`,
|
||||
);
|
||||
} else {
|
||||
await git.init({
|
||||
fs,
|
||||
dir: path,
|
||||
defaultBranch: ref,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitRemove({
|
||||
path,
|
||||
filepath,
|
||||
}: GitFileParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
await execOrThrow(
|
||||
["rm", "-f", "--", filepath],
|
||||
path,
|
||||
`Failed to remove file '${filepath}'`,
|
||||
);
|
||||
} else {
|
||||
await git.remove({
|
||||
fs,
|
||||
dir: path,
|
||||
filepath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGitUncommittedFiles({
|
||||
path,
|
||||
}: GitBaseParams): Promise<string[]> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
const result = await exec(["status", "--porcelain"], path);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get uncommitted files: ${result.stderr.trim() || result.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
return result.stdout
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "")
|
||||
.map((line) => line.slice(3).trim());
|
||||
} else {
|
||||
const statusMatrix = await git.statusMatrix({ fs, dir: path });
|
||||
return statusMatrix
|
||||
.filter((row) => row[1] !== 1 || row[2] !== 1 || row[3] !== 1)
|
||||
.map((row) => row[0]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFileAtCommit({
|
||||
path,
|
||||
filePath,
|
||||
commitHash,
|
||||
}: {
|
||||
path: string;
|
||||
filePath: string;
|
||||
commitHash: string;
|
||||
}): Promise<string | null> {
|
||||
}: GitFileAtCommitParams): Promise<string | null> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`git -C "${path}" show "${commitHash}:${filePath}"`,
|
||||
);
|
||||
return stdout;
|
||||
const result = await exec(["show", `${commitHash}:${filePath}`], path);
|
||||
if (result.exitCode !== 0) {
|
||||
// File doesn't exist at this commit or other error
|
||||
return null;
|
||||
}
|
||||
return result.stdout;
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`Error getting file at commit ${commitHash}: ${error.message}`,
|
||||
@@ -238,3 +372,312 @@ export async function getFileAtCommit({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitListBranches({
|
||||
path,
|
||||
}: GitBaseParams): Promise<string[]> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
const result = await exec(["branch", "--list"], path);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(result.stderr.toString());
|
||||
}
|
||||
// Parse output:
|
||||
// e.g. "* main\n feature/login"
|
||||
return result.stdout
|
||||
.toString()
|
||||
.split("\n")
|
||||
.map((line) => line.replace("*", "").trim())
|
||||
.filter((line) => line.length > 0);
|
||||
} else {
|
||||
return await git.listBranches({
|
||||
fs,
|
||||
dir: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitRenameBranch({
|
||||
path,
|
||||
oldBranch,
|
||||
newBranch,
|
||||
}: GitBranchRenameParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
// git branch -m oldBranch newBranch
|
||||
const result = await exec(["branch", "-m", oldBranch, newBranch], path);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(result.stderr.toString());
|
||||
}
|
||||
} else {
|
||||
await git.renameBranch({
|
||||
fs,
|
||||
dir: path,
|
||||
oldref: oldBranch,
|
||||
ref: newBranch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitClone({
|
||||
path,
|
||||
url,
|
||||
accessToken,
|
||||
singleBranch = true,
|
||||
depth,
|
||||
}: GitCloneParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
// Dugite version (real Git)
|
||||
// Build authenticated URL if accessToken is provided and URL doesn't already have auth
|
||||
const finalUrl =
|
||||
accessToken && !url.includes("@")
|
||||
? url.replace("https://", `https://${accessToken}:x-oauth-basic@`)
|
||||
: url;
|
||||
const args = ["clone"];
|
||||
if (depth && depth > 0) {
|
||||
args.push("--depth", String(depth));
|
||||
}
|
||||
if (singleBranch) {
|
||||
args.push("--single-branch");
|
||||
}
|
||||
args.push(finalUrl, path);
|
||||
const result = await exec(args, ".");
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(result.stderr.toString());
|
||||
}
|
||||
} else {
|
||||
// isomorphic-git version
|
||||
// Strip any embedded auth from URL since isomorphic-git uses onAuth
|
||||
const cleanUrl = url.replace(/https:\/\/[^@]+@/, "https://");
|
||||
await git.clone({
|
||||
fs,
|
||||
http,
|
||||
dir: path,
|
||||
url: cleanUrl,
|
||||
onAuth: accessToken
|
||||
? () => ({
|
||||
username: accessToken,
|
||||
password: "x-oauth-basic",
|
||||
})
|
||||
: undefined,
|
||||
singleBranch,
|
||||
depth: depth ?? undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitSetRemoteUrl({
|
||||
path,
|
||||
remoteUrl,
|
||||
}: GitSetRemoteUrlParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
// Dugite version
|
||||
try {
|
||||
// Try to add the remote
|
||||
const result = await exec(["remote", "add", "origin", remoteUrl], path);
|
||||
|
||||
// If remote already exists, update it instead
|
||||
if (result.exitCode !== 0 && result.stderr.includes("already exists")) {
|
||||
const updateResult = await exec(
|
||||
["remote", "set-url", "origin", remoteUrl],
|
||||
path,
|
||||
);
|
||||
|
||||
if (updateResult.exitCode !== 0) {
|
||||
throw new Error(`Failed to update remote: ${updateResult.stderr}`);
|
||||
}
|
||||
} else if (result.exitCode !== 0) {
|
||||
// Handle other errors
|
||||
throw new Error(`Failed to add remote: ${result.stderr}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("Error setting up remote:", error);
|
||||
throw error; // or handle as needed
|
||||
}
|
||||
} else {
|
||||
//isomorphic-git version
|
||||
await git.setConfig({
|
||||
fs,
|
||||
dir: path,
|
||||
path: "remote.origin.url",
|
||||
value: remoteUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitPush({
|
||||
path,
|
||||
branch,
|
||||
accessToken,
|
||||
force,
|
||||
}: GitPushParams): Promise<void> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
// Dugite version
|
||||
try {
|
||||
// Push using the configured origin remote (which already has auth in URL)
|
||||
const args = ["push", "origin", `main:${branch}`];
|
||||
if (force) {
|
||||
args.push("--force");
|
||||
}
|
||||
const result = await exec(args, path);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
const errorMsg = result.stderr.toString() || result.stdout.toString();
|
||||
throw new Error(`Git push failed: ${errorMsg}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("Error during git push:", error);
|
||||
throw new Error(`Git push failed: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
// isomorphic-git version
|
||||
await git.push({
|
||||
fs,
|
||||
http,
|
||||
dir: path,
|
||||
remote: "origin",
|
||||
ref: "main",
|
||||
remoteRef: branch,
|
||||
onAuth: () => ({
|
||||
username: accessToken,
|
||||
password: "x-oauth-basic",
|
||||
}),
|
||||
force: !!force,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitCurrentBranch({
|
||||
path,
|
||||
}: GitBaseParams): Promise<string | null> {
|
||||
const settings = readSettings();
|
||||
if (settings.enableNativeGit) {
|
||||
// Dugite version
|
||||
const result = await exec(["branch", "--show-current"], path);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get current branch: ${result.stderr.trim() || result.stdout.trim()}`,
|
||||
);
|
||||
}
|
||||
const branch = result.stdout.trim() || null;
|
||||
return branch;
|
||||
} else {
|
||||
// isomorphic-git version returns string | undefined
|
||||
const branch = await git.currentBranch({
|
||||
fs,
|
||||
dir: path,
|
||||
fullname: false,
|
||||
});
|
||||
return branch ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitLog({
|
||||
path,
|
||||
depth = 100_000,
|
||||
}: GitLogParams): Promise<GitCommit[]> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
return await gitLogNative(path, depth);
|
||||
} else {
|
||||
// isomorphic-git fallback: this already returns the same structure
|
||||
return await git.log({
|
||||
fs,
|
||||
dir: path,
|
||||
depth,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitIsIgnored({
|
||||
path,
|
||||
filepath,
|
||||
}: GitFileParams): Promise<boolean> {
|
||||
const settings = readSettings();
|
||||
|
||||
if (settings.enableNativeGit) {
|
||||
// Dugite version
|
||||
// git check-ignore file
|
||||
const result = await exec(["check-ignore", filepath], path);
|
||||
|
||||
// If exitCode == 0 → file is ignored
|
||||
if (result.exitCode === 0) return true;
|
||||
|
||||
// If exitCode == 1 → not ignored
|
||||
if (result.exitCode === 1) return false;
|
||||
|
||||
// Other exit codes are actual errors
|
||||
throw new Error(result.stderr.toString());
|
||||
} else {
|
||||
// isomorphic-git version
|
||||
return await git.isIgnored({
|
||||
fs,
|
||||
dir: path,
|
||||
filepath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function gitLogNative(
|
||||
path: string,
|
||||
depth = 100_000,
|
||||
): Promise<GitCommit[]> {
|
||||
// Use git log with custom format to get all data in a single process
|
||||
// Format: %H = commit hash, %at = author timestamp (unix), %B = raw body (message)
|
||||
// Using null byte as field separator and custom delimiter between commits
|
||||
const logArgs = [
|
||||
"log",
|
||||
"--max-count",
|
||||
String(depth),
|
||||
"--format=%H%x00%at%x00%B%x00---END-COMMIT---",
|
||||
"HEAD",
|
||||
];
|
||||
|
||||
const logResult = await exec(logArgs, path);
|
||||
|
||||
if (logResult.exitCode !== 0) {
|
||||
throw new Error(logResult.stderr.toString());
|
||||
}
|
||||
|
||||
const output = logResult.stdout.toString().trim();
|
||||
if (!output) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Split by commit delimiter (without newline since trim() removes trailing newline)
|
||||
const commitChunks = output.split("\x00---END-COMMIT---").filter(Boolean);
|
||||
const entries: GitCommit[] = [];
|
||||
|
||||
for (const chunk of commitChunks) {
|
||||
// Split by null byte: [oid, timestamp, message]
|
||||
const parts = chunk.split("\x00");
|
||||
if (parts.length >= 3) {
|
||||
const oid = parts[0].trim();
|
||||
const timestamp = Number(parts[1]);
|
||||
// Message is everything after the second null byte, may contain null bytes itself
|
||||
const message = parts.slice(2).join("\x00");
|
||||
|
||||
entries.push({
|
||||
oid,
|
||||
commit: {
|
||||
message: message,
|
||||
author: {
|
||||
timestamp: timestamp,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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}`);
|
||||
|
||||
|
||||
17
src/main.ts
17
src/main.ts
@@ -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>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user