Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
3558663a
Unverified
提交
3558663a
authored
6月 05, 2025
作者:
Will Chen
提交者:
GitHub
6月 05, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Support native Git (experimental) (#338)
上级
4e38031a
隐藏空白字符变更
内嵌
并排
正在显示
25 个修改的文件
包含
465 行增加
和
96 行删除
+465
-96
delete_app.spec.ts
e2e-tests/delete_app.spec.ts
+1
-3
AI_RULES.md
e2e-tests/fixtures/import-app/version-integrity/AI_RULES.md
+1
-0
a.txt
e2e-tests/fixtures/import-app/version-integrity/a.txt
+2
-0
b.txt
e2e-tests/fixtures/import-app/version-integrity/b.txt
+2
-0
c.txt
e2e-tests/fixtures/import-app/version-integrity/dir/c.txt
+2
-0
to-be-deleted.txt
...s/fixtures/import-app/version-integrity/to-be-deleted.txt
+2
-0
to-be-edited.txt
...ts/fixtures/import-app/version-integrity/to-be-edited.txt
+2
-0
version-integrity-add-edit-delete.md
e2e-tests/fixtures/version-integrity-add-edit-delete.md
+10
-0
version-integrity-move-file.md
e2e-tests/fixtures/version-integrity-move-file.md
+2
-0
test_helper.ts
e2e-tests/helpers/test_helper.ts
+104
-3
switch_versions.spec.ts_v1
e2e-tests/snapshots/switch_versions.spec.ts_v1
+8
-0
switch_versions.spec.ts_v2
e2e-tests/snapshots/switch_versions.spec.ts_v2
+5
-0
version_integrity.spec.ts_v1
e2e-tests/snapshots/version_integrity.spec.ts_v1
+19
-0
version_integrity.spec.ts_v2
e2e-tests/snapshots/version_integrity.spec.ts_v2
+20
-0
version_integrity.spec.ts_v3
e2e-tests/snapshots/version_integrity.spec.ts_v3
+20
-0
switch_versions.spec.ts
e2e-tests/switch_versions.spec.ts
+14
-6
template-create-nextjs.spec.ts
e2e-tests/template-create-nextjs.spec.ts
+1
-1
version_integrity.spec.ts
e2e-tests/version_integrity.spec.ts
+51
-0
app_handlers.ts
src/ipc/handlers/app_handlers.ts
+6
-9
import_handlers.ts
src/ipc/handlers/import_handlers.ts
+4
-5
version_handlers.ts
src/ipc/handlers/version_handlers.ts
+12
-60
response_processor.ts
src/ipc/processors/response_processor.ts
+5
-9
git_utils.ts
src/ipc/utils/git_utils.ts
+140
-0
schemas.ts
src/lib/schemas.ts
+2
-0
settings.tsx
src/pages/settings.tsx
+30
-0
没有找到文件。
e2e-tests/delete_app.spec.ts
浏览文件 @
3558663a
...
...
@@ -20,9 +20,7 @@ test("delete app", async ({ po }) => {
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Delete App"
}).
click
();
// Make sure the app is deleted
await
expect
(
async
()
=>
{
expect
(
await
po
.
getCurrentAppName
()).
toBe
(
"(no app selected)"
);
}).
toPass
();
await
po
.
isCurrentAppNameNone
();
expect
(
fs
.
existsSync
(
appPath
)).
toBe
(
false
);
expect
(
po
.
getAppListItem
({
appName
})).
not
.
toBeVisible
();
});
e2e-tests/fixtures/import-app/version-integrity/AI_RULES.md
0 → 100644
浏览文件 @
3558663a
avoid AI_RULES auto-prompt
e2e-tests/fixtures/import-app/version-integrity/a.txt
0 → 100644
浏览文件 @
3558663a
a
\ No newline at end of file
e2e-tests/fixtures/import-app/version-integrity/b.txt
0 → 100644
浏览文件 @
3558663a
b
\ No newline at end of file
e2e-tests/fixtures/import-app/version-integrity/dir/c.txt
0 → 100644
浏览文件 @
3558663a
dir/c.txt
\ No newline at end of file
e2e-tests/fixtures/import-app/version-integrity/to-be-deleted.txt
0 → 100644
浏览文件 @
3558663a
this file should be deleted
\ No newline at end of file
e2e-tests/fixtures/import-app/version-integrity/to-be-edited.txt
0 → 100644
浏览文件 @
3558663a
before-edit
\ No newline at end of file
e2e-tests/fixtures/version-integrity-add-edit-delete.md
0 → 100644
浏览文件 @
3558663a
Deleting a file
<dyad-delete
path=
"to-be-deleted.txt"
></dyad-delete>
<dyad-write
path=
"new-file.js"
description=
"new file"
>
new-file
end of new-file
</dyad-write>
<dyad-write
path=
"to-be-edited.txt"
description=
"editing file"
>
after-edit
</dyad-write>
e2e-tests/fixtures/version-integrity-move-file.md
0 → 100644
浏览文件 @
3558663a
Moving a file
<dyad-rename
from=
"dir/c.txt"
to=
"new-dir/d.txt"
></dyad-rename>
e2e-tests/helpers/test_helper.ts
浏览文件 @
3558663a
...
...
@@ -15,7 +15,7 @@ export const Timeout = {
MEDIUM
:
os
.
platform
()
===
"win32"
?
30
_000
:
15
_000
,
};
class
PageObject
{
export
class
PageObject
{
private
userDataDir
:
string
;
constructor
(
...
...
@@ -26,11 +26,17 @@ class PageObject {
this
.
userDataDir
=
userDataDir
;
}
async
setUp
({
autoApprove
=
false
}:
{
autoApprove
?:
boolean
}
=
{})
{
async
setUp
({
autoApprove
=
false
,
nativeGit
=
false
,
}:
{
autoApprove
?:
boolean
;
nativeGit
?:
boolean
}
=
{})
{
await
this
.
goToSettingsTab
();
if
(
autoApprove
)
{
await
this
.
toggleAutoApprove
();
}
if
(
nativeGit
)
{
await
this
.
toggleNativeGit
();
}
await
this
.
setUpTestProvider
();
await
this
.
setUpTestModel
();
...
...
@@ -67,6 +73,37 @@ class PageObject {
// await page.getByRole('button', { name: 'Select Folder' }).press('Escape');
}
async
snapshotAppFiles
({
name
}:
{
name
?:
string
}
=
{})
{
const
appPath
=
await
this
.
getCurrentAppPath
();
if
(
!
appPath
||
!
fs
.
existsSync
(
appPath
))
{
throw
new
Error
(
`App path does not exist:
${
appPath
}
`
);
}
await
expect
(()
=>
{
const
filesData
=
generateAppFilesSnapshotData
(
appPath
,
appPath
,
[
".git"
,
"node_modules"
,
// Avoid snapshotting lock files because they are getting generated
// automatically and cause noise, and not super important anyways.
"package-lock.json"
,
"pnpm-lock.yaml"
,
]);
// Sort by relative path to ensure deterministic output
filesData
.
sort
((
a
,
b
)
=>
a
.
relativePath
.
localeCompare
(
b
.
relativePath
));
const
snapshotContent
=
filesData
.
map
((
file
)
=>
`===
${
file
.
relativePath
}
===\n
${
file
.
content
}
`
)
.
join
(
"
\
n
\
n"
);
if
(
name
)
{
expect
(
snapshotContent
).
toMatchSnapshot
(
name
);
}
else
{
expect
(
snapshotContent
).
toMatchSnapshot
();
}
}).
toPass
();
}
async
snapshotMessages
({
replaceDumpPath
=
false
,
}:
{
replaceDumpPath
?:
boolean
}
=
{})
{
...
...
@@ -134,9 +171,10 @@ class PageObject {
return
this
.
page
.
getByTestId
(
"preview-iframe-element"
);
}
async
snapshotPreview
()
{
async
snapshotPreview
(
{
name
}:
{
name
?:
string
}
=
{}
)
{
const
iframe
=
this
.
getPreviewIframeElement
();
await
expect
(
iframe
.
contentFrame
().
locator
(
"body"
)).
toMatchAriaSnapshot
({
name
,
timeout
:
Timeout
.
LONG
,
});
}
...
...
@@ -299,7 +337,21 @@ class PageObject {
return
this
.
page
.
getByTestId
(
`app-list-item-
${
appName
}
`
);
}
async
isCurrentAppNameNone
()
{
await
expect
(
async
()
=>
{
await
expect
(
this
.
getTitleBarAppNameButton
()).
toContainText
(
"no app selected"
,
);
}).
toPass
();
}
async
getCurrentAppName
()
{
// Make sure to wait for the app to be set to avoid a race condition.
await
expect
(
async
()
=>
{
await
expect
(
this
.
getTitleBarAppNameButton
()).
not
.
toContainText
(
"no app selected"
,
);
}).
toPass
();
return
(
await
this
.
getTitleBarAppNameButton
().
textContent
())?.
replace
(
"App: "
,
""
,
...
...
@@ -338,6 +390,10 @@ class PageObject {
await
this
.
page
.
getByRole
(
"switch"
,
{
name
:
"Auto-approve"
}).
click
();
}
async
toggleNativeGit
()
{
await
this
.
page
.
getByRole
(
"switch"
,
{
name
:
"Enable Native Git"
}).
click
();
}
async
snapshotSettings
()
{
const
settings
=
path
.
join
(
this
.
userDataDir
,
"user-settings.json"
);
const
settingsContent
=
fs
.
readFileSync
(
settings
,
"utf-8"
);
...
...
@@ -588,3 +644,48 @@ function prettifyDump(
})
.
join
(
"
\
n
\
n"
);
}
interface
FileSnapshotData
{
relativePath
:
string
;
content
:
string
;
}
function
generateAppFilesSnapshotData
(
currentPath
:
string
,
basePath
:
string
,
ignorePatterns
:
string
[],
):
FileSnapshotData
[]
{
const
entries
=
fs
.
readdirSync
(
currentPath
,
{
withFileTypes
:
true
});
let
files
:
FileSnapshotData
[]
=
[];
// Sort entries for deterministic order
entries
.
sort
((
a
,
b
)
=>
a
.
name
.
localeCompare
(
b
.
name
));
for
(
const
entry
of
entries
)
{
const
entryPath
=
path
.
join
(
currentPath
,
entry
.
name
);
if
(
ignorePatterns
.
includes
(
entry
.
name
))
{
continue
;
}
if
(
entry
.
isDirectory
())
{
files
=
files
.
concat
(
generateAppFilesSnapshotData
(
entryPath
,
basePath
,
ignorePatterns
),
);
}
else
if
(
entry
.
isFile
())
{
const
relativePath
=
path
.
relative
(
basePath
,
entryPath
);
try
{
const
content
=
fs
.
readFileSync
(
entryPath
,
"utf-8"
);
files
.
push
({
relativePath
,
content
});
}
catch
(
error
)
{
// Could be a binary file or permission issue, log and add a placeholder
const
e
=
error
as
Error
;
console
.
warn
(
`Could not read file
${
entryPath
}
:
${
e
.
message
}
`
);
files
.
push
({
relativePath
,
content
:
`[Error reading file:
${
e
.
message
}
]`
,
});
}
}
}
return
files
;
}
e2e-tests/snapshots/switch_versions.spec.ts_v1
0 → 100644
浏览文件 @
3558663a
- region "Notifications (F8)":
- list
- region "Notifications alt+T"
- heading "Welcome to Your Blank App" [level=1]
- paragraph: Start building your amazing project here!
- link "Made with Dyad":
- /url: https://www.dyad.sh/
\ No newline at end of file
e2e-tests/snapshots/switch_versions.spec.ts_v2
0 → 100644
浏览文件 @
3558663a
- region "Notifications (F8)":
- list
- region "Notifications alt+T"
- text: Testing:write-index!
\ No newline at end of file
e2e-tests/snapshots/version_integrity.spec.ts_v1
0 → 100644
浏览文件 @
3558663a
=== a.txt ===
a
=== AI_RULES.md ===
avoid AI_RULES auto-prompt
=== b.txt ===
b
=== dir/c.txt ===
dir/c.txt
=== to-be-deleted.txt ===
this file should be deleted
=== to-be-edited.txt ===
before-edit
\ No newline at end of file
e2e-tests/snapshots/version_integrity.spec.ts_v2
0 → 100644
浏览文件 @
3558663a
=== a.txt ===
a
=== AI_RULES.md ===
avoid AI_RULES auto-prompt
=== b.txt ===
b
=== dir/c.txt ===
dir/c.txt
=== new-file.js ===
new-file
end of new-file
=== to-be-edited.txt ===
after-edit
\ No newline at end of file
e2e-tests/snapshots/version_integrity.spec.ts_v3
0 → 100644
浏览文件 @
3558663a
=== a.txt ===
a
=== AI_RULES.md ===
avoid AI_RULES auto-prompt
=== b.txt ===
b
=== new-dir/d.txt ===
dir/c.txt
=== new-file.js ===
new-file
end of new-file
=== to-be-edited.txt ===
after-edit
\ No newline at end of file
e2e-tests/switch_versions.spec.ts
浏览文件 @
3558663a
import
{
testSkipIfWindows
}
from
"./helpers/test_helper"
;
import
{
PageObject
,
testSkipIfWindows
}
from
"./helpers/test_helper"
;
import
{
expect
}
from
"@playwright/test"
;
testSkipIfWindows
(
"switch versions"
,
async
({
po
}
)
=>
{
await
po
.
setUp
({
autoApprove
:
true
});
const
runSwitchVersionTest
=
async
(
po
:
PageObject
,
nativeGit
:
boolean
)
=>
{
await
po
.
setUp
({
autoApprove
:
true
,
nativeGit
});
await
po
.
sendPrompt
(
"tc=write-index"
);
await
po
.
snapshotPreview
();
await
po
.
snapshotPreview
(
{
name
:
`v2`
}
);
expect
(
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Version"
}).
textContent
(),
).
toBe
(
"Version 2"
);
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Version"
}).
click
();
await
po
.
page
.
getByText
(
"Init Dyad app Undo"
).
click
();
await
po
.
snapshotPreview
();
await
po
.
snapshotPreview
(
{
name
:
`v1`
}
);
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Undo to latest version"
}).
click
();
// Should be same as the previous snapshot, but just to be sure.
await
po
.
snapshotPreview
();
await
po
.
snapshotPreview
(
{
name
:
`v1`
}
);
await
expect
(
po
.
page
.
getByText
(
"Version 3"
)).
toBeVisible
();
};
testSkipIfWindows
(
"switch versions"
,
async
({
po
})
=>
{
await
runSwitchVersionTest
(
po
,
false
);
});
testSkipIfWindows
(
"switch versions with native git"
,
async
({
po
})
=>
{
await
runSwitchVersionTest
(
po
,
true
);
});
e2e-tests/template-create-nextjs.spec.ts
浏览文件 @
3558663a
...
...
@@ -15,6 +15,6 @@ test("create next.js app", async ({ po }) => {
await
po
.
clickRestart
();
// This can be pretty slow because it's waiting for the app to build.
await
expect
(
po
.
getPreviewIframeElement
()).
toBeVisible
({
timeout
:
5
0
_000
});
await
expect
(
po
.
getPreviewIframeElement
()).
toBeVisible
({
timeout
:
10
0
_000
});
await
po
.
snapshotPreview
();
});
e2e-tests/version_integrity.spec.ts
0 → 100644
浏览文件 @
3558663a
import
{
PageObject
,
testSkipIfWindows
}
from
"./helpers/test_helper"
;
import
*
as
eph
from
"electron-playwright-helpers"
;
import
path
from
"node:path"
;
const
runVersionIntegrityTest
=
async
(
po
:
PageObject
,
nativeGit
:
boolean
)
=>
{
await
po
.
setUp
({
autoApprove
:
true
,
nativeGit
});
// Importing a simple app with a few files.
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Import App"
}).
click
();
await
eph
.
stubDialog
(
po
.
electronApp
,
"showOpenDialog"
,
{
filePaths
:
[
path
.
join
(
__dirname
,
"fixtures"
,
"import-app"
,
"version-integrity"
),
],
});
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Select Folder"
}).
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Enter new app name"
}).
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Enter new app name"
})
.
fill
(
"version-integrity-app"
);
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Import"
}).
click
();
// Initial snapshot
await
po
.
snapshotAppFiles
({
name
:
"v1"
});
// Add a file and delete a file
await
po
.
sendPrompt
(
"tc=version-integrity-add-edit-delete"
);
await
po
.
snapshotAppFiles
({
name
:
"v2"
});
// Move a file
await
po
.
sendPrompt
(
"tc=version-integrity-move-file"
);
await
po
.
snapshotAppFiles
({
name
:
"v3"
});
// Open version pane
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Version 3"
}).
click
();
await
po
.
page
.
getByText
(
"Init Dyad app Undo"
).
click
();
await
po
.
snapshotAppFiles
({
name
:
"v1"
});
await
po
.
page
.
getByRole
(
"button"
,
{
name
:
"Undo to latest version"
}).
click
();
// Should be same as the previous snapshot, but just to be sure.
await
po
.
snapshotAppFiles
({
name
:
"v1"
});
};
testSkipIfWindows
(
"version integrity (git isomorphic)"
,
async
({
po
})
=>
{
await
runVersionIntegrityTest
(
po
,
false
);
});
testSkipIfWindows
(
"version integrity (git native)"
,
async
({
po
})
=>
{
await
runVersionIntegrityTest
(
po
,
true
);
});
src/ipc/handlers/app_handlers.ts
浏览文件 @
3558663a
...
...
@@ -23,7 +23,7 @@ import { getEnvVar } from "../utils/read_env";
import
{
readSettings
}
from
"../../main/settings"
;
import
fixPath
from
"fix-path"
;
import
{
getGitAuthor
}
from
"../utils/git_author"
;
import
killPort
from
"kill-port"
;
import
util
from
"util"
;
import
log
from
"electron-log"
;
...
...
@@ -33,6 +33,7 @@ import { getLanguageModelProviders } from "../shared/language_model_helpers";
import
{
startProxy
}
from
"../utils/start_proxy_server"
;
import
{
Worker
}
from
"worker_threads"
;
import
{
createFromTemplate
}
from
"./createFromTemplate"
;
import
{
gitCommit
}
from
"../utils/git_utils"
;
const
logger
=
log
.
scope
(
"app_handlers"
);
const
handle
=
createLoggedHandler
(
logger
);
...
...
@@ -207,11 +208,9 @@ export function registerAppHandlers() {
});
// Create initial commit
const
commitHash
=
await
git
.
commit
({
fs
:
fs
,
dir
:
fullAppPath
,
const
commitHash
=
await
gitCommit
({
path
:
fullAppPath
,
message
:
"Init Dyad app"
,
author
:
await
getGitAuthor
(),
});
// Update chat with initial commit hash
...
...
@@ -521,11 +520,9 @@ export function registerAppHandlers() {
filepath
:
filePath
,
});
await
git
.
commit
({
fs
,
dir
:
appPath
,
await
gitCommit
({
path
:
appPath
,
message
:
`Updated
${
filePath
}
`
,
author
:
await
getGitAuthor
(),
});
}
...
...
src/ipc/handlers/import_handlers.ts
浏览文件 @
3558663a
...
...
@@ -9,9 +9,10 @@ import { db } from "@/db";
import
{
chats
}
from
"@/db/schema"
;
import
{
eq
}
from
"drizzle-orm"
;
import
git
from
"isomorphic-git"
;
import
{
getGitAuthor
}
from
"../utils/git_author"
;
import
{
ImportAppParams
,
ImportAppResult
}
from
"../ipc_types"
;
import
{
copyDirectoryRecursive
}
from
"../utils/file_utils"
;
import
{
gitCommit
}
from
"../utils/git_utils"
;
const
logger
=
log
.
scope
(
"import-handlers"
);
const
handle
=
createLoggedHandler
(
logger
);
...
...
@@ -114,11 +115,9 @@ export function registerImportHandlers() {
});
// Create initial commit
await
git
.
commit
({
fs
:
fs
,
dir
:
destPath
,
await
gitCommit
({
path
:
destPath
,
message
:
"Init Dyad app"
,
author
:
await
getGitAuthor
(),
});
}
...
...
src/ipc/handlers/version_handlers.ts
浏览文件 @
3558663a
...
...
@@ -5,12 +5,11 @@ import type { Version, BranchResult } from "../ipc_types";
import
fs
from
"node:fs"
;
import
path
from
"node:path"
;
import
{
getDyadAppPath
}
from
"../../paths/paths"
;
import
git
from
"isomorphic-git"
;
import
{
promises
as
fsPromises
}
from
"node:fs"
;
import
git
,
{
type
ReadCommitResult
}
from
"isomorphic-git"
;
import
{
withLock
}
from
"../utils/lock_utils"
;
import
{
getGitAuthor
}
from
"../utils/git_author"
;
import
log
from
"electron-log"
;
import
{
createLoggedHandler
}
from
"./safe_handle"
;
import
{
gitCheckout
,
gitCommit
,
gitStageToRevert
}
from
"../utils/git_utils"
;
const
logger
=
log
.
scope
(
"version_handlers"
);
...
...
@@ -40,7 +39,7 @@ export function registerVersionHandlers() {
depth
:
10
_000
,
// Limit to last 10_000 commits for performance
});
return
commits
.
map
((
commit
)
=>
({
return
commits
.
map
((
commit
:
ReadCommitResult
)
=>
({
oid
:
commit
.
oid
,
message
:
commit
.
commit
.
message
,
timestamp
:
commit
.
commit
.
author
.
timestamp
,
...
...
@@ -102,65 +101,19 @@ export function registerVersionHandlers() {
const
appPath
=
getDyadAppPath
(
app
.
path
);
await
git
.
checkout
({
fs
,
dir
:
appPath
,
await
gitCheckout
({
path
:
appPath
,
ref
:
"main"
,
force
:
true
,
});
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
const
matrix
=
await
git
.
statusMatrix
({
fs
,
dir
:
appPath
,
ref
:
previousVersionId
,
});
// Process each file to revert to the state in previousVersionId
for
(
const
[
filepath
,
headStatus
,
workdirStatus
]
of
matrix
)
{
const
fullPath
=
path
.
join
(
appPath
,
filepath
);
// If file exists in HEAD (previous version)
if
(
headStatus
===
1
)
{
// If file doesn't exist or has changed in working directory, restore it from the target commit
if
(
workdirStatus
!==
1
)
{
const
{
blob
}
=
await
git
.
readBlob
({
fs
,
dir
:
appPath
,
oid
:
previousVersionId
,
filepath
,
});
await
fsPromises
.
mkdir
(
path
.
dirname
(
fullPath
),
{
recursive
:
true
,
});
await
fsPromises
.
writeFile
(
fullPath
,
Buffer
.
from
(
blob
));
}
}
// If file doesn't exist in HEAD but exists in working directory, delete it
else
if
(
headStatus
===
0
&&
workdirStatus
!==
0
)
{
if
(
fs
.
existsSync
(
fullPath
))
{
await
fsPromises
.
unlink
(
fullPath
);
await
git
.
remove
({
fs
,
dir
:
appPath
,
filepath
:
filepath
,
});
}
}
}
// Stage all changes
await
git
.
add
({
fs
,
dir
:
appPath
,
filepath
:
"."
,
await
gitStageToRevert
({
path
:
appPath
,
targetOid
:
previousVersionId
,
});
// Create a revert commit
await
git
.
commit
({
fs
,
dir
:
appPath
,
await
gitCommit
({
path
:
appPath
,
message
:
`Reverted all changes back to version
${
previousVersionId
}
`
,
author
:
await
getGitAuthor
(),
});
// Find the chat and message associated with the commit hash
...
...
@@ -221,9 +174,8 @@ export function registerVersionHandlers() {
const
appPath
=
getDyadAppPath
(
app
.
path
);
await
git
.
checkout
({
fs
,
dir
:
appPath
,
await
gitCheckout
({
path
:
appPath
,
ref
:
versionId
,
});
});
...
...
src/ipc/processors/response_processor.ts
浏览文件 @
3558663a
...
...
@@ -6,7 +6,6 @@ import { getDyadAppPath } from "../../paths/paths";
import
path
from
"node:path"
;
import
git
from
"isomorphic-git"
;
import
{
getGitAuthor
}
from
"../utils/git_author"
;
import
log
from
"electron-log"
;
import
{
executeAddDependency
}
from
"./executeAddDependency"
;
import
{
...
...
@@ -16,6 +15,7 @@ import {
}
from
"../../supabase_admin/supabase_management_client"
;
import
{
isServerFunction
}
from
"../../supabase_admin/supabase_utils"
;
import
{
SqlQuery
}
from
"../../lib/schemas"
;
import
{
gitCommit
}
from
"../utils/git_utils"
;
const
readFile
=
fs
.
promises
.
readFile
;
const
logger
=
log
.
scope
(
"response_processor"
);
...
...
@@ -460,11 +460,9 @@ export async function processFullResponseActions(
?
`[dyad]
${
chatSummary
}
-
${
changes
.
join
(
", "
)}
`
:
`[dyad]
${
changes
.
join
(
", "
)}
`
;
// Use chat summary, if provided, or default for commit message
let
commitHash
=
await
git
.
commit
({
fs
,
dir
:
appPath
,
let
commitHash
=
await
gitCommit
({
path
:
appPath
,
message
,
author
:
await
getGitAuthor
(),
});
logger
.
log
(
`Successfully committed changes:
${
changes
.
join
(
", "
)}
`
);
...
...
@@ -482,11 +480,9 @@ export async function processFullResponseActions(
filepath
:
"."
,
});
try
{
commitHash
=
await
git
.
commit
({
fs
,
dir
:
appPath
,
commitHash
=
await
gitCommit
({
path
:
appPath
,
message
:
message
+
" + extra files edited outside of Dyad"
,
author
:
await
getGitAuthor
(),
amend
:
true
,
});
logger
.
log
(
...
...
src/ipc/utils/git_utils.ts
0 → 100644
浏览文件 @
3558663a
import
{
getGitAuthor
}
from
"./git_author"
;
import
git
from
"isomorphic-git"
;
import
fs
from
"node:fs"
;
import
{
promises
as
fsPromises
}
from
"node:fs"
;
import
pathModule
from
"node:path"
;
import
{
exec
}
from
"node:child_process"
;
import
{
promisify
}
from
"node:util"
;
import
{
readSettings
}
from
"../../main/settings"
;
const
execAsync
=
promisify
(
exec
);
export
async
function
gitCommit
({
path
,
message
,
amend
,
}:
{
path
:
string
;
message
:
string
;
amend
?:
boolean
;
}):
Promise
<
string
>
{
const
settings
=
readSettings
();
if
(
settings
.
enableNativeGit
)
{
let
command
=
`git -C "
${
path
}
" commit -m "
${
message
.
replace
(
/"/g
,
'
\\
"'
)}
"`
;
if
(
amend
)
{
command
+=
" --amend"
;
}
await
execAsync
(
command
);
const
{
stdout
}
=
await
execAsync
(
`git -C "
${
path
}
" rev-parse HEAD`
);
return
stdout
.
trim
();
}
else
{
return
git
.
commit
({
fs
:
fs
,
dir
:
path
,
message
,
author
:
await
getGitAuthor
(),
amend
:
amend
,
});
}
}
export
async
function
gitCheckout
({
path
,
ref
,
}:
{
path
:
string
;
ref
:
string
;
}):
Promise
<
void
>
{
const
settings
=
readSettings
();
if
(
settings
.
enableNativeGit
)
{
await
execAsync
(
`git -C "
${
path
}
" checkout "
${
ref
.
replace
(
/"/g
,
'
\\
"'
)}
"`
);
return
;
}
else
{
return
git
.
checkout
({
fs
,
dir
:
path
,
ref
});
}
}
export
async
function
gitStageToRevert
({
path
,
targetOid
,
}:
{
path
:
string
;
targetOid
:
string
;
}):
Promise
<
void
>
{
const
settings
=
readSettings
();
if
(
settings
.
enableNativeGit
)
{
// Get the current HEAD commit hash
const
{
stdout
:
currentHead
}
=
await
execAsync
(
`git -C "
${
path
}
" rev-parse HEAD`
,
);
const
currentCommit
=
currentHead
.
trim
();
// If we're already at the target commit, nothing to do
if
(
currentCommit
===
targetOid
)
{
return
;
}
// Safety: refuse to run if the work-tree isn’t clean.
const
{
stdout
:
wtStatus
}
=
await
execAsync
(
`git -C "
${
path
}
" status --porcelain`
,
);
if
(
wtStatus
.
trim
()
!==
""
)
{
throw
new
Error
(
"Cannot revert: working tree has uncommitted changes."
);
}
// Reset the working directory and index to match the target commit state
// This effectively undoes all changes since the target commit
await
execAsync
(
`git -C "
${
path
}
" reset --hard "
${
targetOid
}
"`
);
// Reset back to the original HEAD but keep the working directory as it is
// This stages all the changes needed to revert to the target state
await
execAsync
(
`git -C "
${
path
}
" reset --soft "
${
currentCommit
}
"`
);
}
else
{
// Get status matrix comparing the target commit (previousVersionId as HEAD) with current working directory
const
matrix
=
await
git
.
statusMatrix
({
fs
,
dir
:
path
,
ref
:
targetOid
,
});
// Process each file to revert to the state in previousVersionId
for
(
const
[
filepath
,
headStatus
,
workdirStatus
]
of
matrix
)
{
const
fullPath
=
pathModule
.
join
(
path
,
filepath
);
// If file exists in HEAD (previous version)
if
(
headStatus
===
1
)
{
// If file doesn't exist or has changed in working directory, restore it from the target commit
if
(
workdirStatus
!==
1
)
{
const
{
blob
}
=
await
git
.
readBlob
({
fs
,
dir
:
path
,
oid
:
targetOid
,
filepath
,
});
await
fsPromises
.
mkdir
(
pathModule
.
dirname
(
fullPath
),
{
recursive
:
true
,
});
await
fsPromises
.
writeFile
(
fullPath
,
Buffer
.
from
(
blob
));
}
}
// If file doesn't exist in HEAD but exists in working directory, delete it
else
if
(
headStatus
===
0
&&
workdirStatus
!==
0
)
{
if
(
fs
.
existsSync
(
fullPath
))
{
await
fsPromises
.
unlink
(
fullPath
);
await
git
.
remove
({
fs
,
dir
:
path
,
filepath
:
filepath
,
});
}
}
}
// Stage all changes
await
git
.
add
({
fs
,
dir
:
path
,
filepath
:
"."
,
});
}
}
src/lib/schemas.ts
浏览文件 @
3558663a
...
...
@@ -122,6 +122,8 @@ export const UserSettingsSchema = z.object({
enableProSmartFilesContextMode
:
z
.
boolean
().
optional
(),
selectedTemplateId
:
z
.
string
().
optional
(),
enableNativeGit
:
z
.
boolean
().
optional
(),
////////////////////////////////
// E2E TESTING ONLY.
////////////////////////////////
...
...
src/pages/settings.tsx
浏览文件 @
3558663a
...
...
@@ -15,6 +15,7 @@ import { useRouter } from "@tanstack/react-router";
import
{
GitHubIntegration
}
from
"@/components/GitHubIntegration"
;
import
{
SupabaseIntegration
}
from
"@/components/SupabaseIntegration"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Label
}
from
"@/components/ui/label"
;
export
default
function
SettingsPage
()
{
const
{
theme
,
setTheme
}
=
useTheme
();
...
...
@@ -108,6 +109,35 @@ export default function SettingsPage() {
</
div
>
</
div
>
<
div
className=
"space-y-1 mt-4"
>
<
div
className=
"flex items-center space-x-2"
>
<
Switch
id=
"enable-native-git"
checked=
{
!!
settings
?.
enableNativeGit
}
onCheckedChange=
{
(
checked
)
=>
{
updateSettings
({
enableNativeGit
:
checked
,
});
}
}
/>
<
Label
htmlFor=
"enable-native-git"
>
Enable Native Git
</
Label
>
</
div
>
<
div
className=
"text-sm text-gray-500 dark:text-gray-400"
>
(Experimental) 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
>
.
</
div
>
</
div
>
<
div
className=
"mt-4"
>
<
MaxChatTurnsSelector
/>
</
div
>
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论