mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add size-based progress tracking for worktree file copying (#10871)
This commit is contained in:
parent
d87abe8efc
commit
971885d8e7
25 changed files with 379 additions and 53 deletions
|
|
@ -264,5 +264,45 @@ describe("WorktreeIncludeService", () => {
|
|||
|
||||
expect(result).toContain("node_modules")
|
||||
})
|
||||
|
||||
it("should call progress callback with size-based progress", async () => {
|
||||
// Set up files to copy
|
||||
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules\n.env.local")
|
||||
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules\n.env.local")
|
||||
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
|
||||
await fs.writeFile(path.join(sourceDir, "node_modules", "test.txt"), "test")
|
||||
await fs.writeFile(path.join(sourceDir, ".env.local"), "LOCAL_VAR=value")
|
||||
|
||||
const progressCalls: Array<{ bytesCopied: number; totalBytes: number; itemName: string }> = []
|
||||
const onProgress = vi.fn((progress: { bytesCopied: number; totalBytes: number; itemName: string }) => {
|
||||
progressCalls.push({ ...progress })
|
||||
})
|
||||
|
||||
await service.copyWorktreeIncludeFiles(sourceDir, targetDir, onProgress)
|
||||
|
||||
// Should be called multiple times (initial + after each copy + during polling)
|
||||
expect(onProgress).toHaveBeenCalled()
|
||||
|
||||
// All calls should have totalBytes > 0 (since we have files)
|
||||
expect(progressCalls.every((p) => p.totalBytes > 0)).toBe(true)
|
||||
|
||||
// Final call should have bytesCopied === totalBytes (complete)
|
||||
const finalCall = progressCalls[progressCalls.length - 1]
|
||||
expect(finalCall?.bytesCopied).toBe(finalCall?.totalBytes)
|
||||
|
||||
// Each call should have an item name
|
||||
expect(progressCalls.every((p) => typeof p.itemName === "string")).toBe(true)
|
||||
})
|
||||
|
||||
it("should not fail when progress callback is not provided", async () => {
|
||||
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
|
||||
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
|
||||
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
|
||||
|
||||
// Should not throw when no callback is provided
|
||||
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
|
||||
|
||||
expect(result).toContain("node_modules")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,4 +10,4 @@ export * from "./types.js"
|
|||
|
||||
// Services
|
||||
export { WorktreeService, worktreeService } from "./worktree-service.js"
|
||||
export { WorktreeIncludeService, worktreeIncludeService } from "./worktree-include.js"
|
||||
export { WorktreeIncludeService, worktreeIncludeService, type CopyProgressCallback } from "./worktree-include.js"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Used to copy untracked files (like node_modules) when creating worktrees.
|
||||
*/
|
||||
|
||||
import { execFile } from "child_process"
|
||||
import { execFile, spawn } from "child_process"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { promisify } from "util"
|
||||
|
|
@ -14,6 +14,23 @@ import ignore, { type Ignore } from "ignore"
|
|||
|
||||
import type { WorktreeIncludeStatus } from "./types.js"
|
||||
|
||||
/**
|
||||
* Progress info for size-based copy tracking.
|
||||
*/
|
||||
export interface CopyProgress {
|
||||
/** Current bytes copied */
|
||||
bytesCopied: number
|
||||
/** Total bytes to copy */
|
||||
totalBytes: number
|
||||
/** Name of current item being copied */
|
||||
itemName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for reporting copy progress during worktree file copying.
|
||||
*/
|
||||
export type CopyProgressCallback = (progress: CopyProgress) => void
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
/**
|
||||
|
|
@ -93,9 +110,16 @@ export class WorktreeIncludeService {
|
|||
* Copy files matching .worktreeinclude patterns from source to target.
|
||||
* Only copies files that are ALSO in .gitignore (to avoid copying tracked files).
|
||||
*
|
||||
* @param sourceDir - The source directory containing the files to copy
|
||||
* @param targetDir - The target directory where files will be copied
|
||||
* @param onProgress - Optional callback to report copy progress (size-based)
|
||||
* @returns Array of copied file/directory paths
|
||||
*/
|
||||
async copyWorktreeIncludeFiles(sourceDir: string, targetDir: string): Promise<string[]> {
|
||||
async copyWorktreeIncludeFiles(
|
||||
sourceDir: string,
|
||||
targetDir: string,
|
||||
onProgress?: CopyProgressCallback,
|
||||
): Promise<string[]> {
|
||||
const worktreeIncludePath = path.join(sourceDir, ".worktreeinclude")
|
||||
const gitignorePath = path.join(sourceDir, ".gitignore")
|
||||
|
||||
|
|
@ -136,9 +160,30 @@ export class WorktreeIncludeService {
|
|||
// Find items that match BOTH patterns (intersection)
|
||||
const itemsToCopy = await this.findMatchingItems(sourceDir, worktreeIncludeMatcher, gitignoreMatcher)
|
||||
|
||||
// Copy the items
|
||||
if (itemsToCopy.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Calculate total size of all items to copy (for accurate progress)
|
||||
const itemSizes = await Promise.all(
|
||||
itemsToCopy.map(async (item) => {
|
||||
const sourcePath = path.join(sourceDir, item)
|
||||
const size = await this.getPathSize(sourcePath)
|
||||
return { item, size }
|
||||
}),
|
||||
)
|
||||
|
||||
const totalBytes = itemSizes.reduce((sum, { size }) => sum + size, 0)
|
||||
let bytesCopied = 0
|
||||
|
||||
// Report initial progress
|
||||
if (onProgress && totalBytes > 0) {
|
||||
onProgress({ bytesCopied: 0, totalBytes, itemName: itemsToCopy[0]! })
|
||||
}
|
||||
|
||||
// Copy the items with size-based progress tracking
|
||||
const copiedItems: string[] = []
|
||||
for (const item of itemsToCopy) {
|
||||
for (const { item, size } of itemSizes) {
|
||||
const sourcePath = path.join(sourceDir, item)
|
||||
const targetPath = path.join(targetDir, item)
|
||||
|
||||
|
|
@ -146,23 +191,197 @@ export class WorktreeIncludeService {
|
|||
const stats = await fs.stat(sourcePath)
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
// Use native cp for directories (much faster)
|
||||
await this.copyDirectoryNative(sourcePath, targetPath)
|
||||
// Use native cp for directories with progress polling
|
||||
await this.copyDirectoryWithProgress(
|
||||
sourcePath,
|
||||
targetPath,
|
||||
item,
|
||||
bytesCopied,
|
||||
totalBytes,
|
||||
onProgress,
|
||||
)
|
||||
} else {
|
||||
// Report progress before copying
|
||||
onProgress?.({ bytesCopied, totalBytes, itemName: item })
|
||||
|
||||
// Ensure parent directory exists
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true })
|
||||
await fs.copyFile(sourcePath, targetPath)
|
||||
}
|
||||
|
||||
bytesCopied += size
|
||||
copiedItems.push(item)
|
||||
|
||||
// Report progress after copying
|
||||
onProgress?.({ bytesCopied, totalBytes, itemName: item })
|
||||
} catch (error) {
|
||||
// Log but don't fail on individual copy errors
|
||||
console.error(`Failed to copy ${item}:`, error)
|
||||
// Still count the size as "processed" to avoid progress getting stuck
|
||||
bytesCopied += size
|
||||
}
|
||||
}
|
||||
|
||||
return copiedItems
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size on disk of a file (accounts for filesystem block allocation).
|
||||
* Uses blksize to calculate actual disk usage including block overhead.
|
||||
*/
|
||||
private getSizeOnDisk(stats: { size: number; blksize?: number }): number {
|
||||
// Calculate size on disk using filesystem block size
|
||||
if (stats.blksize !== undefined && stats.blksize > 0) {
|
||||
return stats.blksize * Math.ceil(stats.size / stats.blksize)
|
||||
}
|
||||
// Fallback to logical size when blksize not available
|
||||
return stats.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total size on disk of a file or directory (recursively).
|
||||
* Uses native Node.js fs operations for cross-platform compatibility.
|
||||
*/
|
||||
private async getPathSize(targetPath: string): Promise<number> {
|
||||
try {
|
||||
const stats = await fs.stat(targetPath)
|
||||
|
||||
if (stats.isFile()) {
|
||||
return this.getSizeOnDisk(stats)
|
||||
}
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
return await this.getDirectorySizeRecursive(targetPath)
|
||||
}
|
||||
|
||||
return 0
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively calculate directory size on disk using Node.js fs.
|
||||
* Uses parallel processing for better performance on large directories.
|
||||
*/
|
||||
private async getDirectorySizeRecursive(dirPath: string): Promise<number> {
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
const sizes = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = path.join(dirPath, entry.name)
|
||||
try {
|
||||
if (entry.isFile()) {
|
||||
const stats = await fs.stat(entryPath)
|
||||
return this.getSizeOnDisk(stats)
|
||||
} else if (entry.isDirectory()) {
|
||||
return await this.getDirectorySizeRecursive(entryPath)
|
||||
}
|
||||
return 0
|
||||
} catch {
|
||||
return 0 // Skip inaccessible files
|
||||
}
|
||||
}),
|
||||
)
|
||||
return sizes.reduce((sum, size) => sum + size, 0)
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current size of a directory (for progress tracking).
|
||||
*/
|
||||
private async getCurrentDirectorySize(dirPath: string): Promise<number> {
|
||||
try {
|
||||
await fs.access(dirPath)
|
||||
return await this.getDirectorySizeRecursive(dirPath)
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy directory with progress polling.
|
||||
* Starts native copy and polls target directory size to report progress.
|
||||
*/
|
||||
private async copyDirectoryWithProgress(
|
||||
source: string,
|
||||
target: string,
|
||||
itemName: string,
|
||||
bytesCopiedBefore: number,
|
||||
totalBytes: number,
|
||||
onProgress?: CopyProgressCallback,
|
||||
): Promise<void> {
|
||||
// Ensure parent directory exists
|
||||
await fs.mkdir(path.dirname(target), { recursive: true })
|
||||
|
||||
const isWindows = process.platform === "win32"
|
||||
const expectedSize = await this.getPathSize(source)
|
||||
|
||||
// Start the copy process
|
||||
const copyPromise = new Promise<void>((resolve, reject) => {
|
||||
let proc: ReturnType<typeof spawn>
|
||||
|
||||
if (isWindows) {
|
||||
proc = spawn("robocopy", [source, target, "/E", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP"], {
|
||||
windowsHide: true,
|
||||
})
|
||||
} else {
|
||||
proc = spawn("cp", ["-r", "--", source, target])
|
||||
}
|
||||
|
||||
proc.on("close", (code) => {
|
||||
if (isWindows) {
|
||||
// robocopy returns non-zero for success (values < 8)
|
||||
if (code !== null && code < 8) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`robocopy failed with code ${code}`))
|
||||
}
|
||||
} else {
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`cp failed with code ${code}`))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
proc.on("error", reject)
|
||||
})
|
||||
|
||||
// Poll progress while copying
|
||||
const pollInterval = 500 // Poll every 500ms
|
||||
let polling = true
|
||||
|
||||
const pollProgress = async () => {
|
||||
while (polling) {
|
||||
const currentSize = await this.getCurrentDirectorySize(target)
|
||||
const totalCopied = bytesCopiedBefore + currentSize
|
||||
|
||||
onProgress?.({
|
||||
bytesCopied: Math.min(totalCopied, bytesCopiedBefore + expectedSize),
|
||||
totalBytes,
|
||||
itemName,
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval))
|
||||
}
|
||||
}
|
||||
|
||||
// Start polling and wait for copy to complete
|
||||
const pollPromise = pollProgress()
|
||||
|
||||
try {
|
||||
await copyPromise
|
||||
} finally {
|
||||
polling = false
|
||||
// Wait for final poll iteration to complete
|
||||
await pollPromise.catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .gitignore-style file and return the patterns
|
||||
*/
|
||||
|
|
@ -213,43 +432,6 @@ export class WorktreeIncludeService {
|
|||
|
||||
return matchingItems
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy directory using native cp command for performance.
|
||||
* This is 10-20x faster than Node.js fs.cp for large directories like node_modules.
|
||||
*/
|
||||
private async copyDirectoryNative(source: string, target: string): Promise<void> {
|
||||
// Ensure parent directory exists
|
||||
await fs.mkdir(path.dirname(target), { recursive: true })
|
||||
|
||||
// Use platform-appropriate copy command
|
||||
const isWindows = process.platform === "win32"
|
||||
|
||||
if (isWindows) {
|
||||
// Use robocopy on Windows (more reliable than xcopy)
|
||||
// robocopy returns non-zero for success, so we check the exit code
|
||||
try {
|
||||
await execFileAsync(
|
||||
"robocopy",
|
||||
[source, target, "/E", "/NFL", "/NDL", "/NJH", "/NJS", "/nc", "/ns", "/np"],
|
||||
{ windowsHide: true },
|
||||
)
|
||||
} catch (error) {
|
||||
// robocopy returns non-zero for success (values < 8)
|
||||
const exitCode =
|
||||
typeof (error as { code?: unknown }).code === "number"
|
||||
? (error as { code: number }).code
|
||||
: undefined
|
||||
if (exitCode !== undefined && exitCode < 8) {
|
||||
return // Success
|
||||
}
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
// Use cp -r on Unix-like systems
|
||||
await execFileAsync("cp", ["-r", "--", source, target])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance for convenience
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export interface ExtensionMessage {
|
|||
// Worktree response types
|
||||
| "worktreeList"
|
||||
| "worktreeResult"
|
||||
| "worktreeCopyProgress"
|
||||
| "branchList"
|
||||
| "worktreeDefaults"
|
||||
| "worktreeIncludeStatus"
|
||||
|
|
@ -257,6 +258,10 @@ export interface ExtensionMessage {
|
|||
// branchWorktreeIncludeResult
|
||||
branch?: string
|
||||
hasWorktreeInclude?: boolean
|
||||
// worktreeCopyProgress (size-based)
|
||||
copyProgressBytesCopied?: number
|
||||
copyProgressTotalBytes?: number
|
||||
copyProgressItemName?: string
|
||||
}
|
||||
|
||||
export interface OpenAiCodexRateLimitsMessage {
|
||||
|
|
|
|||
|
|
@ -3375,12 +3375,23 @@ export const webviewMessageHandler = async (
|
|||
|
||||
case "createWorktree": {
|
||||
try {
|
||||
const { success, message: text } = await handleCreateWorktree(provider, {
|
||||
path: message.worktreePath!,
|
||||
branch: message.worktreeBranch,
|
||||
baseBranch: message.worktreeBaseBranch,
|
||||
createNewBranch: message.worktreeCreateNewBranch,
|
||||
})
|
||||
const { success, message: text } = await handleCreateWorktree(
|
||||
provider,
|
||||
{
|
||||
path: message.worktreePath!,
|
||||
branch: message.worktreeBranch,
|
||||
baseBranch: message.worktreeBaseBranch,
|
||||
createNewBranch: message.worktreeCreateNewBranch,
|
||||
},
|
||||
(progress) => {
|
||||
provider.postMessageToWebview({
|
||||
type: "worktreeCopyProgress",
|
||||
copyProgressBytesCopied: progress.bytesCopied,
|
||||
copyProgressTotalBytes: progress.totalBytes,
|
||||
copyProgressItemName: progress.itemName,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
await provider.postMessageToWebview({ type: "worktreeResult", success, text })
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import type {
|
|||
WorktreeListResponse,
|
||||
WorktreeDefaultsResponse,
|
||||
} from "@roo-code/types"
|
||||
import { worktreeService, worktreeIncludeService } from "@roo-code/core"
|
||||
import { worktreeService, worktreeIncludeService, type CopyProgressCallback } from "@roo-code/core"
|
||||
|
||||
import type { ClineProvider } from "../ClineProvider"
|
||||
|
||||
|
|
@ -137,6 +137,7 @@ export async function handleCreateWorktree(
|
|||
baseBranch?: string
|
||||
createNewBranch?: boolean
|
||||
},
|
||||
onCopyProgress?: CopyProgressCallback,
|
||||
): Promise<WorktreeResult> {
|
||||
const cwd = provider.cwd
|
||||
|
||||
|
|
@ -154,7 +155,11 @@ export async function handleCreateWorktree(
|
|||
// If successful and .worktreeinclude exists, copy the files.
|
||||
if (result.success && result.worktree) {
|
||||
try {
|
||||
const copiedItems = await worktreeIncludeService.copyWorktreeIncludeFiles(cwd, result.worktree.path)
|
||||
const copiedItems = await worktreeIncludeService.copyWorktreeIncludeFiles(
|
||||
cwd,
|
||||
result.worktree.path,
|
||||
onCopyProgress,
|
||||
)
|
||||
if (copiedItems.length > 0) {
|
||||
result.message += ` (copied ${copiedItems.length} item(s) from .worktreeinclude)`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect, useCallback, useMemo } from "react"
|
||||
import prettyBytes from "pretty-bytes"
|
||||
|
||||
import type { WorktreeDefaultsResponse, BranchInfo, WorktreeIncludeStatus } from "@roo-code/types"
|
||||
|
||||
|
|
@ -44,6 +45,11 @@ export const CreateWorktreeModal = ({
|
|||
// UI state
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [copyProgress, setCopyProgress] = useState<{
|
||||
bytesCopied: number
|
||||
totalBytes: number
|
||||
itemName: string
|
||||
} | null>(null)
|
||||
|
||||
// Fetch defaults and branches on open
|
||||
useEffect(() => {
|
||||
|
|
@ -76,8 +82,17 @@ export const CreateWorktreeModal = ({
|
|||
setIncludeStatus(message.worktreeIncludeStatus)
|
||||
break
|
||||
}
|
||||
case "worktreeCopyProgress": {
|
||||
setCopyProgress({
|
||||
bytesCopied: message.copyProgressBytesCopied ?? 0,
|
||||
totalBytes: message.copyProgressTotalBytes ?? 0,
|
||||
itemName: message.copyProgressItemName ?? "",
|
||||
})
|
||||
break
|
||||
}
|
||||
case "worktreeResult": {
|
||||
setIsCreating(false)
|
||||
setCopyProgress(null)
|
||||
if (message.success) {
|
||||
if (openAfterCreate) {
|
||||
vscode.postMessage({
|
||||
|
|
@ -207,10 +222,42 @@ export const CreateWorktreeModal = ({
|
|||
<p className="text-vscode-errorForeground">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress section - appears during file copying */}
|
||||
{copyProgress && (
|
||||
<div className="flex flex-col gap-2 px-3 py-3 rounded-lg bg-vscode-editor-background border border-vscode-panel-border">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-vscode-foreground font-medium">
|
||||
{t("worktrees:copyingFiles")}
|
||||
</span>
|
||||
<span className="text-vscode-descriptionForeground">
|
||||
{copyProgress.totalBytes > 0
|
||||
? Math.round((copyProgress.bytesCopied / copyProgress.totalBytes) * 100)
|
||||
: 0}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-vscode-input-background rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-vscode-button-background rounded-full transition-all duration-200"
|
||||
style={{
|
||||
width: `${copyProgress.totalBytes > 0 ? (copyProgress.bytesCopied / copyProgress.totalBytes) * 100 : 0}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-vscode-descriptionForeground truncate">
|
||||
{t("worktrees:copyingProgress", {
|
||||
item: copyProgress.itemName,
|
||||
copied: prettyBytes(copyProgress.bytesCopied),
|
||||
total: prettyBytes(copyProgress.totalBytes),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
<Button variant="secondary" onClick={onClose} disabled={isCreating}>
|
||||
{t("worktrees:cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleCreate} disabled={!isValid || isCreating}>
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ca/worktrees.json
generated
2
webview-ui/src/i18n/locales/ca/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Sense un fitxer .worktreeinclude, fitxers com node_modules no es copiaran al worktree nou. Potser hauràs d'executar npm install després de crear-lo.",
|
||||
"create": "Crea",
|
||||
"creating": "S'està creant...",
|
||||
"copyingFiles": "S'estan copiant fitxers...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Cancel·la",
|
||||
|
||||
"deleteWorktree": "Suprimeix worktree",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/de/worktrees.json
generated
2
webview-ui/src/i18n/locales/de/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Ohne eine .worktreeinclude-Datei werden Dateien wie node_modules nicht in den neuen Worktree kopiert. Möglicherweise musst du nach dem Erstellen npm install ausführen.",
|
||||
"create": "Erstellen",
|
||||
"creating": "Wird erstellt...",
|
||||
"copyingFiles": "Dateien werden kopiert...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Abbrechen",
|
||||
|
||||
"deleteWorktree": "Worktree löschen",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Without a .worktreeinclude file, files like node_modules won't be copied to the new worktree. You may need to run npm install after creating it.",
|
||||
"create": "Create",
|
||||
"creating": "Creating...",
|
||||
"copyingFiles": "Copying files...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Cancel",
|
||||
|
||||
"deleteWorktree": "Delete Worktree",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/es/worktrees.json
generated
2
webview-ui/src/i18n/locales/es/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Sin un archivo .worktreeinclude, archivos como node_modules no se copiarán al worktree nuevo. Puede que tengas que ejecutar npm install después de crearlo.",
|
||||
"create": "Crear",
|
||||
"creating": "Creando...",
|
||||
"copyingFiles": "Copiando archivos...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Cancelar",
|
||||
|
||||
"deleteWorktree": "Eliminar Worktree",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/fr/worktrees.json
generated
2
webview-ui/src/i18n/locales/fr/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Sans fichier .worktreeinclude, des fichiers comme node_modules ne seront pas copiés dans le nouveau worktree. Tu devras peut-être exécuter npm install après l'avoir créé.",
|
||||
"create": "Créer",
|
||||
"creating": "Création...",
|
||||
"copyingFiles": "Copie des fichiers...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Annuler",
|
||||
|
||||
"deleteWorktree": "Supprimer le worktree",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/hi/worktrees.json
generated
2
webview-ui/src/i18n/locales/hi/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": ".worktreeinclude फ़ाइल के बिना, node_modules जैसी फ़ाइलें नए worktree में कॉपी नहीं होंगी। इसे बनाने के बाद आपको npm install चलाना पड़ सकता है।",
|
||||
"create": "बनाएँ",
|
||||
"creating": "बनाया जा रहा है...",
|
||||
"copyingFiles": "फ़ाइलें कॉपी की जा रही हैं...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "रद्द करें",
|
||||
|
||||
"deleteWorktree": "Worktree हटाएँ",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/id/worktrees.json
generated
2
webview-ui/src/i18n/locales/id/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Tanpa file .worktreeinclude, file seperti node_modules tidak akan disalin ke worktree baru. Kamu mungkin perlu menjalankan npm install setelah membuatnya.",
|
||||
"create": "Buat",
|
||||
"creating": "Membuat...",
|
||||
"copyingFiles": "Menyalin file...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Batal",
|
||||
|
||||
"deleteWorktree": "Hapus Worktree",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/it/worktrees.json
generated
2
webview-ui/src/i18n/locales/it/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Senza un file .worktreeinclude, file come node_modules non verranno copiati nel nuovo worktree. Potresti dover eseguire npm install dopo averlo creato.",
|
||||
"create": "Crea",
|
||||
"creating": "Creazione...",
|
||||
"copyingFiles": "Copia dei file...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Annulla",
|
||||
|
||||
"deleteWorktree": "Elimina Worktree",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ja/worktrees.json
generated
2
webview-ui/src/i18n/locales/ja/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": ".worktreeinclude ファイルがない場合、node_modules などのファイルは新しい worktree にコピーされません。作成後に npm install を実行する必要があるかもしれません。",
|
||||
"create": "作成",
|
||||
"creating": "作成中...",
|
||||
"copyingFiles": "ファイルをコピー中...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "キャンセル",
|
||||
|
||||
"deleteWorktree": "Worktree を削除",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ko/worktrees.json
generated
2
webview-ui/src/i18n/locales/ko/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": ".worktreeinclude 파일이 없으면 node_modules 같은 파일이 새 worktree로 복사되지 않습니다. 생성 후 npm install을 실행해야 할 수도 있습니다.",
|
||||
"create": "만들기",
|
||||
"creating": "만드는 중...",
|
||||
"copyingFiles": "파일 복사 중...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "취소",
|
||||
|
||||
"deleteWorktree": "Worktree 삭제",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/nl/worktrees.json
generated
2
webview-ui/src/i18n/locales/nl/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Zonder een .worktreeinclude-bestand worden bestanden zoals node_modules niet naar de nieuwe worktree gekopieerd. Mogelijk moet je na het aanmaken npm install uitvoeren.",
|
||||
"create": "Aanmaken",
|
||||
"creating": "Bezig met aanmaken...",
|
||||
"copyingFiles": "Bestanden kopiëren...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Annuleren",
|
||||
|
||||
"deleteWorktree": "Worktree verwijderen",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/pl/worktrees.json
generated
2
webview-ui/src/i18n/locales/pl/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Bez pliku .worktreeinclude pliki takie jak node_modules nie zostaną skopiowane do nowego worktree. Po utworzeniu może być konieczne uruchomienie npm install.",
|
||||
"create": "Utwórz",
|
||||
"creating": "Tworzenie...",
|
||||
"copyingFiles": "Kopiowanie plików...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Anuluj",
|
||||
|
||||
"deleteWorktree": "Usuń Worktree",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/pt-BR/worktrees.json
generated
2
webview-ui/src/i18n/locales/pt-BR/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Sem um arquivo .worktreeinclude, arquivos como node_modules não serão copiados para o novo worktree. Você pode precisar executar npm install depois de criá-lo.",
|
||||
"create": "Criar",
|
||||
"creating": "Criando...",
|
||||
"copyingFiles": "Copiando arquivos...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Cancelar",
|
||||
|
||||
"deleteWorktree": "Excluir Worktree",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ru/worktrees.json
generated
2
webview-ui/src/i18n/locales/ru/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Без файла .worktreeinclude файлы вроде node_modules не будут скопированы в новый worktree. Возможно, после создания нужно будет выполнить npm install.",
|
||||
"create": "Создать",
|
||||
"creating": "Создание...",
|
||||
"copyingFiles": "Копирование файлов...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Отмена",
|
||||
|
||||
"deleteWorktree": "Удалить Worktree",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/tr/worktrees.json
generated
2
webview-ui/src/i18n/locales/tr/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": ".worktreeinclude dosyası olmadan node_modules gibi dosyalar yeni worktree'ye kopyalanmaz. Oluşturduktan sonra npm install çalıştırman gerekebilir.",
|
||||
"create": "Oluştur",
|
||||
"creating": "Oluşturuluyor...",
|
||||
"copyingFiles": "Dosyalar kopyalanıyor...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "İptal",
|
||||
|
||||
"deleteWorktree": "Worktree'yi sil",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/vi/worktrees.json
generated
2
webview-ui/src/i18n/locales/vi/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "Không có tệp .worktreeinclude thì các tệp như node_modules sẽ không được sao chép sang worktree mới. Bạn có thể cần chạy npm install sau khi tạo.",
|
||||
"create": "Tạo",
|
||||
"creating": "Đang tạo...",
|
||||
"copyingFiles": "Đang sao chép tệp...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "Hủy",
|
||||
|
||||
"deleteWorktree": "Xóa Worktree",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/zh-CN/worktrees.json
generated
2
webview-ui/src/i18n/locales/zh-CN/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "没有 .worktreeinclude 文件时,node_modules 等文件不会复制到新 worktree。创建后你可能需要运行 npm install。",
|
||||
"create": "创建",
|
||||
"creating": "正在创建...",
|
||||
"copyingFiles": "正在复制文件...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "取消",
|
||||
|
||||
"deleteWorktree": "删除 Worktree",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/zh-TW/worktrees.json
generated
2
webview-ui/src/i18n/locales/zh-TW/worktrees.json
generated
|
|
@ -42,6 +42,8 @@
|
|||
"noIncludeFileHint": "沒有 .worktreeinclude 檔案時,node_modules 等檔案不會複製到新的 worktree。建立後你可能需要執行 npm install。",
|
||||
"create": "建立",
|
||||
"creating": "正在建立...",
|
||||
"copyingFiles": "正在複製檔案...",
|
||||
"copyingProgress": "{{item}} — {{copied}} / {{total}}",
|
||||
"cancel": "取消",
|
||||
|
||||
"deleteWorktree": "刪除 Worktree",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue