添加git推送窗口,显示待提交列表和查看文件差异
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Capability for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": ["welcome", "folder-*", "commit-*", "merge-*", "stash-*", "settings"],
|
"windows": ["welcome", "folder-*", "commit-*", "merge-*", "stash-*", "push-*", "settings"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window:default",
|
"core:window:default",
|
||||||
|
|||||||
@@ -597,3 +597,42 @@ pub async fn open_stash_window(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_push_window(
|
||||||
|
app: AppHandle,
|
||||||
|
db: tauri::State<'_, AppDatabase>,
|
||||||
|
folder_id: i32,
|
||||||
|
) -> Result<(), AppCommandError> {
|
||||||
|
let label = format!("push-{folder_id}");
|
||||||
|
|
||||||
|
if let Some(existing) = app.get_webview_window(&label) {
|
||||||
|
ensure_windows_undecorated(&existing);
|
||||||
|
let _ = existing.unminimize();
|
||||||
|
existing
|
||||||
|
.set_focus()
|
||||||
|
.map_err(|e| AppCommandError::window("Failed to focus push window", e.to_string()))?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let folder = crate::db::service::folder_service::get_folder_by_id(&db.conn, folder_id)
|
||||||
|
.await
|
||||||
|
.map_err(AppCommandError::from)?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
AppCommandError::not_found(format!("Folder {folder_id} not found"))
|
||||||
|
.with_detail(format!("folder_id={folder_id}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let url = WebviewUrl::App(format!("push?folderId={folder_id}").into());
|
||||||
|
let builder = WebviewWindowBuilder::new(&app, &label, url)
|
||||||
|
.title(format!("Push - {}", folder.name))
|
||||||
|
.inner_size(1100.0, 700.0)
|
||||||
|
.min_inner_size(800.0, 500.0)
|
||||||
|
.center();
|
||||||
|
let push_window = apply_platform_window_style(builder)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| AppCommandError::window("Failed to open push window", e.to_string()))?;
|
||||||
|
ensure_windows_undecorated(&push_window);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ pub fn run() {
|
|||||||
windows::focus_folder_window,
|
windows::focus_folder_window,
|
||||||
windows::open_merge_window,
|
windows::open_merge_window,
|
||||||
windows::open_stash_window,
|
windows::open_stash_window,
|
||||||
|
windows::open_push_window,
|
||||||
system_settings::get_system_proxy_settings,
|
system_settings::get_system_proxy_settings,
|
||||||
system_settings::update_system_proxy_settings,
|
system_settings::update_system_proxy_settings,
|
||||||
system_settings::get_system_language_settings,
|
system_settings::get_system_language_settings,
|
||||||
|
|||||||
8
src/app/push/layout.tsx
Normal file
8
src/app/push/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { ReactNode } from "react"
|
||||||
|
import { GitCredentialProvider } from "@/contexts/git-credential-context"
|
||||||
|
|
||||||
|
export default function PushLayout({ children }: { children: ReactNode }) {
|
||||||
|
return <GitCredentialProvider>{children}</GitCredentialProvider>
|
||||||
|
}
|
||||||
111
src/app/push/page.tsx
Normal file
111
src/app/push/page.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Suspense, useEffect, useState } from "react"
|
||||||
|
import { useSearchParams } from "next/navigation"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
import { Loader2 } from "lucide-react"
|
||||||
|
import { PushWorkspace } from "@/components/layout/push-workspace"
|
||||||
|
import { AppTitleBar } from "@/components/layout/app-title-bar"
|
||||||
|
import { AppToaster } from "@/components/ui/app-toaster"
|
||||||
|
import { getFolder } from "@/lib/tauri"
|
||||||
|
import type { FolderDetail } from "@/lib/types"
|
||||||
|
|
||||||
|
const TOAST_DURATION_MS = 6000
|
||||||
|
|
||||||
|
interface FolderLoadState {
|
||||||
|
loadedId: number | null
|
||||||
|
folder: FolderDetail | null
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function PushPageInner() {
|
||||||
|
const t = useTranslations("Folder.pushWindow")
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const [state, setState] = useState<FolderLoadState>({
|
||||||
|
loadedId: null,
|
||||||
|
folder: null,
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const folderId = Number(searchParams.get("folderId") ?? "0")
|
||||||
|
const normalizedFolderId = Number.isFinite(folderId) ? folderId : 0
|
||||||
|
const hasValidFolderId = normalizedFolderId > 0
|
||||||
|
const loading = hasValidFolderId && state.loadedId !== normalizedFolderId
|
||||||
|
const folder = state.loadedId === normalizedFolderId ? state.folder : null
|
||||||
|
const error = state.loadedId === normalizedFolderId ? state.error : null
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasValidFolderId) return
|
||||||
|
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
getFolder(normalizedFolderId)
|
||||||
|
.then((detail) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setState({
|
||||||
|
loadedId: normalizedFolderId,
|
||||||
|
folder: detail,
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setState({
|
||||||
|
loadedId: normalizedFolderId,
|
||||||
|
folder: null,
|
||||||
|
error: String(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [hasValidFolderId, normalizedFolderId])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen flex-col overflow-hidden bg-background text-foreground">
|
||||||
|
<AppTitleBar
|
||||||
|
center={
|
||||||
|
<div className="text-sm font-semibold tracking-tight">
|
||||||
|
{t("title")}
|
||||||
|
{hasValidFolderId && folder ? ` · ${folder.name}` : ""}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<main className="min-h-0 flex-1">
|
||||||
|
{!hasValidFolderId ? (
|
||||||
|
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||||
|
Invalid folder ID
|
||||||
|
</div>
|
||||||
|
) : loading ? (
|
||||||
|
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
) : folder ? (
|
||||||
|
<PushWorkspace folderPath={folder.path} folderName={folder.name} />
|
||||||
|
) : null}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<AppToaster
|
||||||
|
position="bottom-right"
|
||||||
|
duration={TOAST_DURATION_MS}
|
||||||
|
closeButton
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PushPage() {
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<PushPageInner />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -67,7 +67,6 @@ import {
|
|||||||
gitInit,
|
gitInit,
|
||||||
gitPull,
|
gitPull,
|
||||||
gitFetch,
|
gitFetch,
|
||||||
gitPush,
|
|
||||||
gitNewBranch,
|
gitNewBranch,
|
||||||
gitWorktreeAdd,
|
gitWorktreeAdd,
|
||||||
gitCheckout,
|
gitCheckout,
|
||||||
@@ -78,9 +77,8 @@ import {
|
|||||||
openFolderWindow,
|
openFolderWindow,
|
||||||
openCommitWindow,
|
openCommitWindow,
|
||||||
setFolderParentBranch,
|
setFolderParentBranch,
|
||||||
gitListConflicts,
|
|
||||||
gitHasMergeHead,
|
|
||||||
openStashWindow,
|
openStashWindow,
|
||||||
|
openPushWindow,
|
||||||
} from "@/lib/tauri"
|
} from "@/lib/tauri"
|
||||||
import { RemoteManageDialog } from "@/components/layout/remote-manage-dialog"
|
import { RemoteManageDialog } from "@/components/layout/remote-manage-dialog"
|
||||||
import { ConflictDialog } from "@/components/layout/conflict-dialog"
|
import { ConflictDialog } from "@/components/layout/conflict-dialog"
|
||||||
@@ -300,138 +298,6 @@ export function BranchDropdown({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uses operation "merge" intentionally: MERGE_HEAD exists so merge state is
|
|
||||||
// already active. MergeWorkspace won't call gitStartPullMerge (only for "pull"),
|
|
||||||
// and ConflictDialog abort correctly runs git merge --abort.
|
|
||||||
async function showMergeConflictDialog() {
|
|
||||||
try {
|
|
||||||
const remaining = await gitListConflicts(folderPath)
|
|
||||||
setConflictInfo({
|
|
||||||
has_conflicts: true,
|
|
||||||
conflicted_files: remaining,
|
|
||||||
operation: "merge",
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
setConflictInfo({
|
|
||||||
has_conflicts: true,
|
|
||||||
conflicted_files: [],
|
|
||||||
operation: "merge",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handlePush() {
|
|
||||||
// Pre-check: if MERGE_HEAD exists, show conflict dialog immediately
|
|
||||||
try {
|
|
||||||
if (await gitHasMergeHead(folderPath)) {
|
|
||||||
await showMergeConflictDialog()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Pre-check failed, continue with normal push flow
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskId = `git-${++taskSeq.current}-${Date.now()}`
|
|
||||||
const label = t("tasks.pushCode")
|
|
||||||
setLoading(true)
|
|
||||||
addTask(taskId, label)
|
|
||||||
updateTask(taskId, { status: "running" })
|
|
||||||
try {
|
|
||||||
const result = await withCredentialRetry(
|
|
||||||
(creds) => gitPush(folderPath, creds),
|
|
||||||
{ folderPath }
|
|
||||||
)
|
|
||||||
updateTask(taskId, { status: "completed" })
|
|
||||||
onBranchChange()
|
|
||||||
let description: string | undefined
|
|
||||||
if (result.upstream_set) {
|
|
||||||
description =
|
|
||||||
result.pushed_commits === 0
|
|
||||||
? t("toasts.upstreamSet")
|
|
||||||
: t("toasts.upstreamSetAndPushed", {
|
|
||||||
count: result.pushed_commits,
|
|
||||||
})
|
|
||||||
} else if (result.pushed_commits === 0) {
|
|
||||||
description = t("toasts.noCommitsToPush")
|
|
||||||
} else {
|
|
||||||
description = t("toasts.pushedCommits", {
|
|
||||||
count: result.pushed_commits,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
toast.success(t("toasts.taskCompleted", { label }), {
|
|
||||||
description,
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
const errorMsg = toErrorMessage(err)
|
|
||||||
if (/MERGE_HEAD|unfinished merge/i.test(errorMsg)) {
|
|
||||||
// Unfinished merge — show conflict dialog
|
|
||||||
removeTask(taskId)
|
|
||||||
await showMergeConflictDialog()
|
|
||||||
} else if (/rejected|fetch first/i.test(errorMsg)) {
|
|
||||||
// Remote has new commits — auto-pull then retry push
|
|
||||||
updateTask(taskId, {
|
|
||||||
status: "running",
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
const pullResult = await withCredentialRetry(
|
|
||||||
(creds) => gitPull(folderPath, creds),
|
|
||||||
{ folderPath }
|
|
||||||
)
|
|
||||||
if (pullResult.conflict?.has_conflicts) {
|
|
||||||
removeTask(taskId)
|
|
||||||
onBranchChange()
|
|
||||||
setConflictInfo(pullResult.conflict)
|
|
||||||
} else {
|
|
||||||
// Pull succeeded, retry push
|
|
||||||
updateTask(taskId, { status: "running" })
|
|
||||||
const pushResult = await withCredentialRetry(
|
|
||||||
(creds) => gitPush(folderPath, creds),
|
|
||||||
{ folderPath }
|
|
||||||
)
|
|
||||||
updateTask(taskId, { status: "completed" })
|
|
||||||
onBranchChange()
|
|
||||||
let description: string | undefined
|
|
||||||
if (pushResult.upstream_set) {
|
|
||||||
description =
|
|
||||||
pushResult.pushed_commits === 0
|
|
||||||
? t("toasts.upstreamSet")
|
|
||||||
: t("toasts.upstreamSetAndPushed", {
|
|
||||||
count: pushResult.pushed_commits,
|
|
||||||
})
|
|
||||||
} else if (pushResult.pushed_commits === 0) {
|
|
||||||
description = t("toasts.noCommitsToPush")
|
|
||||||
} else {
|
|
||||||
description = t("toasts.pushedCommits", {
|
|
||||||
count: pushResult.pushed_commits,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
toast.success(t("toasts.taskCompleted", { label }), {
|
|
||||||
description,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (pullErr) {
|
|
||||||
const pullErrMsg = toErrorMessage(pullErr)
|
|
||||||
if (/MERGE_HEAD|unfinished merge/i.test(pullErrMsg)) {
|
|
||||||
removeTask(taskId)
|
|
||||||
await showMergeConflictDialog()
|
|
||||||
} else {
|
|
||||||
removeTask(taskId)
|
|
||||||
const pullErrTitle = t("toasts.taskFailed", { label })
|
|
||||||
pushAlert("error", pullErrTitle, pullErrMsg)
|
|
||||||
toast.error(pullErrTitle, { description: pullErrMsg })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
removeTask(taskId)
|
|
||||||
const errorTitle = t("toasts.taskFailed", { label })
|
|
||||||
pushAlert("error", errorTitle, errorMsg)
|
|
||||||
toast.error(errorTitle, { description: errorMsg })
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMergeParent() {
|
function handleMergeParent() {
|
||||||
if (!parentBranch) return
|
if (!parentBranch) return
|
||||||
setConfirmAction({ type: "merge", branchName: parentBranch })
|
setConfirmAction({ type: "merge", branchName: parentBranch })
|
||||||
@@ -751,7 +617,19 @@ export function BranchDropdown({
|
|||||||
<GitCommitHorizontal className="h-3.5 w-3.5" />
|
<GitCommitHorizontal className="h-3.5 w-3.5" />
|
||||||
{t("openCommitWindow")}
|
{t("openCommitWindow")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem disabled={loading} onSelect={handlePush}>
|
<DropdownMenuItem
|
||||||
|
disabled={loading}
|
||||||
|
onSelect={() => {
|
||||||
|
if (!folder) return
|
||||||
|
setDropdownOpen(false)
|
||||||
|
openPushWindow(folder.id).catch((err) => {
|
||||||
|
const title = t("toasts.openPushWindowFailed")
|
||||||
|
const msg = toErrorMessage(err)
|
||||||
|
pushAlert("error", title, msg)
|
||||||
|
toast.error(title, { description: msg })
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Upload className="h-3.5 w-3.5" />
|
<Upload className="h-3.5 w-3.5" />
|
||||||
{t("pushCode")}
|
{t("pushCode")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
628
src/components/layout/push-workspace.tsx
Normal file
628
src/components/layout/push-workspace.tsx
Normal file
@@ -0,0 +1,628 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { ReactElement } from "react"
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||||
|
import {
|
||||||
|
ChevronsDownUp,
|
||||||
|
ChevronsUpDown,
|
||||||
|
CloudOff,
|
||||||
|
Loader2,
|
||||||
|
Upload,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import {
|
||||||
|
ResizableHandle,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizablePanelGroup,
|
||||||
|
} from "@/components/ui/resizable"
|
||||||
|
import {
|
||||||
|
FileTree,
|
||||||
|
FileTreeFile,
|
||||||
|
FileTreeFolder,
|
||||||
|
} from "@/components/ai-elements/file-tree"
|
||||||
|
import {
|
||||||
|
Commit,
|
||||||
|
CommitContent,
|
||||||
|
CommitFileAdditions,
|
||||||
|
CommitFileChanges,
|
||||||
|
CommitFileDeletions,
|
||||||
|
CommitFileIcon,
|
||||||
|
CommitFileInfo,
|
||||||
|
CommitFilePath,
|
||||||
|
CommitFiles,
|
||||||
|
CommitFileStatus,
|
||||||
|
CommitHash,
|
||||||
|
CommitHeader,
|
||||||
|
CommitInfo,
|
||||||
|
CommitMessage,
|
||||||
|
CommitMetadata,
|
||||||
|
CommitTimestamp,
|
||||||
|
} from "@/components/ai-elements/commit"
|
||||||
|
import { DiffViewer } from "@/components/diff/diff-viewer"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { gitLog, gitPush, gitShowFile } from "@/lib/tauri"
|
||||||
|
import { toErrorMessage } from "@/lib/app-error"
|
||||||
|
import { languageFromPath } from "@/lib/language-detect"
|
||||||
|
import type { GitLogEntry, GitLogFileChange } from "@/lib/types"
|
||||||
|
import { useGitCredential } from "@/contexts/git-credential-context"
|
||||||
|
|
||||||
|
// --- File tree types & builder (same as aux-panel-git-log-tab) ---
|
||||||
|
|
||||||
|
type CommitFileTreeDirNode = {
|
||||||
|
kind: "dir"
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
children: CommitFileTreeNode[]
|
||||||
|
fileCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommitFileTreeFileNode = {
|
||||||
|
kind: "file"
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
change: GitLogFileChange
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommitFileTreeNode = CommitFileTreeDirNode | CommitFileTreeFileNode
|
||||||
|
|
||||||
|
interface MutableCommitFileTreeDirNode {
|
||||||
|
kind: "dir"
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
children: Map<string, MutableCommitFileTreeDirNode | CommitFileTreeFileNode>
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePathSegments(path: string): string[] {
|
||||||
|
const normalized = path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")
|
||||||
|
if (!normalized) return []
|
||||||
|
return normalized.split("/").filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSortedTreeNodes(
|
||||||
|
dir: MutableCommitFileTreeDirNode
|
||||||
|
): CommitFileTreeNode[] {
|
||||||
|
return Array.from(dir.children.values())
|
||||||
|
.map<CommitFileTreeNode>((node) => {
|
||||||
|
if (node.kind === "file") return node
|
||||||
|
return {
|
||||||
|
kind: "dir" as const,
|
||||||
|
fileCount: 0,
|
||||||
|
name: node.name,
|
||||||
|
path: node.path,
|
||||||
|
children: toSortedTreeNodes(node),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1
|
||||||
|
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function compressAndAnnotateDir(
|
||||||
|
node: CommitFileTreeDirNode
|
||||||
|
): CommitFileTreeDirNode {
|
||||||
|
let compressedChildren: CommitFileTreeNode[] = node.children.map((child) => {
|
||||||
|
if (child.kind === "file") return child
|
||||||
|
return compressAndAnnotateDir(child)
|
||||||
|
})
|
||||||
|
|
||||||
|
let fileCount = compressedChildren.reduce((count, child) => {
|
||||||
|
if (child.kind === "file") return count + 1
|
||||||
|
return count + child.fileCount
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
let nextNode: CommitFileTreeDirNode = {
|
||||||
|
...node,
|
||||||
|
children: compressedChildren,
|
||||||
|
fileCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
while (
|
||||||
|
nextNode.children.length === 1 &&
|
||||||
|
nextNode.children[0].kind === "dir"
|
||||||
|
) {
|
||||||
|
const onlyChild = nextNode.children[0]
|
||||||
|
nextNode = {
|
||||||
|
kind: "dir",
|
||||||
|
name: `${nextNode.name}/${onlyChild.name}`,
|
||||||
|
path: onlyChild.path,
|
||||||
|
children: onlyChild.children,
|
||||||
|
fileCount: onlyChild.fileCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compressedChildren = nextNode.children
|
||||||
|
fileCount = compressedChildren.reduce((count, child) => {
|
||||||
|
if (child.kind === "file") return count + 1
|
||||||
|
return count + child.fileCount
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...nextNode,
|
||||||
|
children: compressedChildren,
|
||||||
|
fileCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCommitFileTree(files: GitLogFileChange[]): CommitFileTreeNode[] {
|
||||||
|
const root: MutableCommitFileTreeDirNode = {
|
||||||
|
kind: "dir",
|
||||||
|
name: "",
|
||||||
|
path: "",
|
||||||
|
children: new Map(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const change of files) {
|
||||||
|
const segments = normalizePathSegments(change.path)
|
||||||
|
if (segments.length === 0) continue
|
||||||
|
|
||||||
|
let current = root
|
||||||
|
for (const [index, segment] of segments.entries()) {
|
||||||
|
const nodePath = segments.slice(0, index + 1).join("/")
|
||||||
|
const isLeaf = index === segments.length - 1
|
||||||
|
|
||||||
|
if (isLeaf) {
|
||||||
|
current.children.set(`file:${nodePath}`, {
|
||||||
|
kind: "file",
|
||||||
|
name: segment,
|
||||||
|
path: nodePath,
|
||||||
|
change,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirKey = `dir:${nodePath}`
|
||||||
|
const existing = current.children.get(dirKey)
|
||||||
|
if (existing && existing.kind === "dir") {
|
||||||
|
current = existing
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextDir: MutableCommitFileTreeDirNode = {
|
||||||
|
kind: "dir",
|
||||||
|
name: segment,
|
||||||
|
path: nodePath,
|
||||||
|
children: new Map(),
|
||||||
|
}
|
||||||
|
current.children.set(dirKey, nextDir)
|
||||||
|
current = nextDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedNodes = toSortedTreeNodes(root)
|
||||||
|
return sortedNodes.map((node) => {
|
||||||
|
if (node.kind === "file") return node
|
||||||
|
return compressAndAnnotateDir(node)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectExpandedDirectoryPaths(
|
||||||
|
nodes: CommitFileTreeNode[],
|
||||||
|
expanded = new Set<string>()
|
||||||
|
): Set<string> {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.kind !== "dir") continue
|
||||||
|
expanded.add(node.path)
|
||||||
|
collectExpandedDirectoryPaths(node.children, expanded)
|
||||||
|
}
|
||||||
|
return expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapFileStatus(
|
||||||
|
status: string
|
||||||
|
): "added" | "modified" | "deleted" | "renamed" {
|
||||||
|
switch (status.toUpperCase().charAt(0)) {
|
||||||
|
case "A":
|
||||||
|
return "added"
|
||||||
|
case "D":
|
||||||
|
return "deleted"
|
||||||
|
case "R":
|
||||||
|
return "renamed"
|
||||||
|
default:
|
||||||
|
return "modified"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRelativeTime(
|
||||||
|
dateStr: string,
|
||||||
|
t: (
|
||||||
|
key:
|
||||||
|
| "time.monthsAgo"
|
||||||
|
| "time.daysAgo"
|
||||||
|
| "time.hoursAgo"
|
||||||
|
| "time.minsAgo"
|
||||||
|
| "time.justNow",
|
||||||
|
values?: { count: number }
|
||||||
|
) => string
|
||||||
|
): string {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
if (Number.isNaN(date.getTime())) return dateStr
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const diffMs = now.getTime() - date.getTime()
|
||||||
|
const diffMin = Math.floor(diffMs / 60_000)
|
||||||
|
const diffHour = Math.floor(diffMin / 60)
|
||||||
|
const diffDay = Math.floor(diffHour / 24)
|
||||||
|
|
||||||
|
if (diffDay > 30) {
|
||||||
|
const diffMonth = Math.floor(diffDay / 30)
|
||||||
|
return t("time.monthsAgo", { count: diffMonth })
|
||||||
|
}
|
||||||
|
if (diffDay > 0) return t("time.daysAgo", { count: diffDay })
|
||||||
|
if (diffHour > 0) return t("time.hoursAgo", { count: diffHour })
|
||||||
|
if (diffMin > 0) return t("time.minsAgo", { count: diffMin })
|
||||||
|
return t("time.justNow", { count: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDate(dateStr: string): Date | null {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main component ---
|
||||||
|
|
||||||
|
interface PushWorkspaceProps {
|
||||||
|
folderPath: string
|
||||||
|
folderName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PushWorkspace({ folderPath, folderName }: PushWorkspaceProps) {
|
||||||
|
const t = useTranslations("Folder.pushWindow")
|
||||||
|
const tLog = useTranslations("Folder.gitLogTab")
|
||||||
|
const { withCredentialRetry } = useGitCredential()
|
||||||
|
|
||||||
|
const [commits, setCommits] = useState<GitLogEntry[]>([])
|
||||||
|
const [listLoading, setListLoading] = useState(false)
|
||||||
|
const [openByCommit, setOpenByCommit] = useState<Record<string, boolean>>({})
|
||||||
|
const [pushing, setPushing] = useState(false)
|
||||||
|
|
||||||
|
const [selectedFile, setSelectedFile] = useState<string | null>(null)
|
||||||
|
const [selectedCommit, setSelectedCommit] = useState<string | null>(null)
|
||||||
|
const [originalContent, setOriginalContent] = useState("")
|
||||||
|
const [modifiedContent, setModifiedContent] = useState("")
|
||||||
|
const [diffLoading, setDiffLoading] = useState(false)
|
||||||
|
|
||||||
|
const unpushedCommits = useMemo(
|
||||||
|
() => commits.filter((c) => c.pushed === false),
|
||||||
|
[commits]
|
||||||
|
)
|
||||||
|
|
||||||
|
const loadCommits = useCallback(async () => {
|
||||||
|
setListLoading(true)
|
||||||
|
try {
|
||||||
|
const entries = await gitLog(folderPath, 100)
|
||||||
|
setCommits(entries)
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(toErrorMessage(err))
|
||||||
|
} finally {
|
||||||
|
setListLoading(false)
|
||||||
|
}
|
||||||
|
}, [folderPath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadCommits()
|
||||||
|
}, [loadCommits])
|
||||||
|
|
||||||
|
async function handleSelectFile(commitHash: string, file: string) {
|
||||||
|
setSelectedFile(file)
|
||||||
|
setSelectedCommit(commitHash)
|
||||||
|
setDiffLoading(true)
|
||||||
|
try {
|
||||||
|
const [orig, mod] = await Promise.all([
|
||||||
|
gitShowFile(folderPath, file, `${commitHash}~1`).catch(() => ""),
|
||||||
|
gitShowFile(folderPath, file, commitHash).catch(() => ""),
|
||||||
|
])
|
||||||
|
setOriginalContent(orig)
|
||||||
|
setModifiedContent(mod)
|
||||||
|
} catch {
|
||||||
|
setOriginalContent("")
|
||||||
|
setModifiedContent("")
|
||||||
|
} finally {
|
||||||
|
setDiffLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePush() {
|
||||||
|
setPushing(true)
|
||||||
|
try {
|
||||||
|
const result = await withCredentialRetry(
|
||||||
|
(creds) => gitPush(folderPath, creds),
|
||||||
|
{ folderPath }
|
||||||
|
)
|
||||||
|
let description: string | undefined
|
||||||
|
if (result.upstream_set) {
|
||||||
|
description =
|
||||||
|
result.pushed_commits === 0
|
||||||
|
? t("toasts.upstreamSet")
|
||||||
|
: t("toasts.upstreamSetAndPushed", {
|
||||||
|
count: result.pushed_commits,
|
||||||
|
})
|
||||||
|
} else if (result.pushed_commits === 0) {
|
||||||
|
description = t("toasts.noCommitsToPush")
|
||||||
|
} else {
|
||||||
|
description = t("toasts.pushedCommits", {
|
||||||
|
count: result.pushed_commits,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toast.success(t("toasts.pushSuccess"), { description })
|
||||||
|
await loadCommits()
|
||||||
|
setSelectedFile(null)
|
||||||
|
setSelectedCommit(null)
|
||||||
|
setOpenByCommit({})
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(t("toasts.pushFailed"), {
|
||||||
|
description: toErrorMessage(err),
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setPushing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<ResizablePanelGroup direction="horizontal" className="min-h-0 flex-1">
|
||||||
|
{/* Left panel: commit list */}
|
||||||
|
<ResizablePanel defaultSize={35} minSize={25}>
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<ScrollArea className="min-h-0 flex-1">
|
||||||
|
{listLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : unpushedCommits.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||||
|
{t("noUnpushedCommits")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2 p-2">
|
||||||
|
{unpushedCommits.map((entry) => {
|
||||||
|
const commitKey = entry.full_hash
|
||||||
|
const commitDate = parseDate(entry.date)
|
||||||
|
const isOpen = !!openByCommit[commitKey]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Commit
|
||||||
|
key={commitKey}
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setOpenByCommit((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[commitKey]: open,
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CommitHeader>
|
||||||
|
<CommitInfo className="min-w-0">
|
||||||
|
<CommitMessage className="line-clamp-1 leading-snug">
|
||||||
|
{entry.message}
|
||||||
|
</CommitMessage>
|
||||||
|
<CommitMetadata className="mt-1 min-w-0 flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="inline-flex shrink-0"
|
||||||
|
title={t("unpushed")}
|
||||||
|
>
|
||||||
|
<CloudOff
|
||||||
|
className="text-amber-500"
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{entry.author}</span>
|
||||||
|
<CommitTimestamp
|
||||||
|
className="shrink-0"
|
||||||
|
date={commitDate ?? new Date()}
|
||||||
|
>
|
||||||
|
{formatRelativeTime(entry.date, tLog)}
|
||||||
|
</CommitTimestamp>
|
||||||
|
<CommitHash className="text-primary/70">
|
||||||
|
{entry.hash}
|
||||||
|
</CommitHash>
|
||||||
|
</CommitMetadata>
|
||||||
|
</CommitInfo>
|
||||||
|
</CommitHeader>
|
||||||
|
<CommitContent>
|
||||||
|
{entry.files.length === 0 ? (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{tLog("noFileChangeDetails")}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<PushCommitFilesTree
|
||||||
|
commitHash={entry.full_hash}
|
||||||
|
files={entry.files}
|
||||||
|
folderName={folderName}
|
||||||
|
onSelectFile={(file) =>
|
||||||
|
handleSelectFile(entry.full_hash, file)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CommitContent>
|
||||||
|
</Commit>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Push button */}
|
||||||
|
<div className="border-t p-2">
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
disabled={pushing || unpushedCommits.length === 0}
|
||||||
|
onClick={handlePush}
|
||||||
|
>
|
||||||
|
{pushing ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Upload className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{t("push")}
|
||||||
|
{unpushedCommits.length > 0 && ` (${unpushedCommits.length})`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
|
||||||
|
<ResizableHandle />
|
||||||
|
|
||||||
|
{/* Right panel: diff viewer */}
|
||||||
|
<ResizablePanel defaultSize={65} minSize={40}>
|
||||||
|
{diffLoading ? (
|
||||||
|
<div className="flex h-full items-center justify-center">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : selectedFile && selectedCommit ? (
|
||||||
|
<DiffViewer
|
||||||
|
original={originalContent}
|
||||||
|
modified={modifiedContent}
|
||||||
|
originalLabel={`${selectedCommit.slice(0, 7)}~ (${t("before")})`}
|
||||||
|
modifiedLabel={`${selectedCommit.slice(0, 7)} (${t("after")})`}
|
||||||
|
language={languageFromPath(selectedFile)}
|
||||||
|
className="h-full"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||||
|
{t("selectFileToViewDiff")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Commit Files Tree for Push Window ---
|
||||||
|
|
||||||
|
function PushCommitFilesTree({
|
||||||
|
commitHash,
|
||||||
|
files,
|
||||||
|
folderName,
|
||||||
|
onSelectFile,
|
||||||
|
}: {
|
||||||
|
commitHash: string
|
||||||
|
files: GitLogFileChange[]
|
||||||
|
folderName: string
|
||||||
|
onSelectFile: (file: string) => void
|
||||||
|
}) {
|
||||||
|
const tLog = useTranslations("Folder.gitLogTab")
|
||||||
|
const rootPath = "__push_file_tree_root__"
|
||||||
|
const treeNodes = useMemo(() => buildCommitFileTree(files), [files])
|
||||||
|
const allDirectoryPaths = useMemo(() => {
|
||||||
|
const paths = collectExpandedDirectoryPaths(treeNodes)
|
||||||
|
paths.add(rootPath)
|
||||||
|
return paths
|
||||||
|
}, [treeNodes])
|
||||||
|
const [expandedPaths, setExpandedPaths] =
|
||||||
|
useState<Set<string>>(allDirectoryPaths)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExpandedPaths(allDirectoryPaths)
|
||||||
|
}, [allDirectoryPaths])
|
||||||
|
|
||||||
|
const canExpandAll = useMemo(() => {
|
||||||
|
if (allDirectoryPaths.size === 0) return false
|
||||||
|
for (const path of allDirectoryPaths) {
|
||||||
|
if (!expandedPaths.has(path)) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, [allDirectoryPaths, expandedPaths])
|
||||||
|
|
||||||
|
const canCollapseAll = expandedPaths.size > 0
|
||||||
|
|
||||||
|
const toggleExpanded = useCallback(() => {
|
||||||
|
if (canExpandAll) {
|
||||||
|
setExpandedPaths(new Set(allDirectoryPaths))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setExpandedPaths(new Set())
|
||||||
|
}, [allDirectoryPaths, canExpandAll])
|
||||||
|
|
||||||
|
const renderNode = (node: CommitFileTreeNode): ReactElement => {
|
||||||
|
if (node.kind === "dir") {
|
||||||
|
return (
|
||||||
|
<FileTreeFolder
|
||||||
|
key={node.path}
|
||||||
|
path={node.path}
|
||||||
|
name={node.name}
|
||||||
|
suffix={`(${node.fileCount})`}
|
||||||
|
suffixClassName="text-muted-foreground/45"
|
||||||
|
title={node.path}
|
||||||
|
>
|
||||||
|
{node.children.map(renderNode)}
|
||||||
|
</FileTreeFolder>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = node.change
|
||||||
|
return (
|
||||||
|
<FileTreeFile
|
||||||
|
key={`${commitHash}:${file.path}`}
|
||||||
|
className="w-full min-w-0 cursor-pointer"
|
||||||
|
name={node.name}
|
||||||
|
onClick={() => onSelectFile(file.path)}
|
||||||
|
path={node.path}
|
||||||
|
title={file.path}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<span className="size-4 shrink-0" />
|
||||||
|
<CommitFileInfo className="flex-1 min-w-0 gap-1.5">
|
||||||
|
<CommitFileStatus status={mapFileStatus(file.status)}>
|
||||||
|
{file.status}
|
||||||
|
</CommitFileStatus>
|
||||||
|
<CommitFileIcon />
|
||||||
|
<CommitFilePath title={file.path}>{node.name}</CommitFilePath>
|
||||||
|
</CommitFileInfo>
|
||||||
|
<CommitFileChanges>
|
||||||
|
<CommitFileAdditions count={file.additions} />
|
||||||
|
<CommitFileDeletions count={file.deletions} />
|
||||||
|
</CommitFileChanges>
|
||||||
|
</>
|
||||||
|
</FileTreeFile>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
{tLog("filesTitle")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-5"
|
||||||
|
onClick={toggleExpanded}
|
||||||
|
disabled={!canExpandAll && !canCollapseAll}
|
||||||
|
title={
|
||||||
|
canExpandAll ? tLog("expandAllFiles") : tLog("collapseAllFiles")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{canExpandAll ? (
|
||||||
|
<ChevronsUpDown className="size-3.5" />
|
||||||
|
) : (
|
||||||
|
<ChevronsDownUp className="size-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CommitFiles>
|
||||||
|
<FileTree
|
||||||
|
className="max-h-[32rem] overflow-auto rounded-md border-border/60 bg-transparent text-xs [&>div]:p-1"
|
||||||
|
expanded={expandedPaths}
|
||||||
|
onExpandedChange={setExpandedPaths}
|
||||||
|
>
|
||||||
|
<FileTreeFolder
|
||||||
|
path={rootPath}
|
||||||
|
name={folderName}
|
||||||
|
suffix={`(${files.length})`}
|
||||||
|
suffixClassName="text-muted-foreground/45"
|
||||||
|
title={folderName}
|
||||||
|
>
|
||||||
|
{treeNodes.map(renderNode)}
|
||||||
|
</FileTreeFolder>
|
||||||
|
</FileTree>
|
||||||
|
</CommitFiles>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -882,6 +882,7 @@
|
|||||||
"allFilesUpToDate": "كل الملفات محدثة",
|
"allFilesUpToDate": "كل الملفات محدثة",
|
||||||
"updatedFiles": "{count, plural, one {# ملف تم تحديثه} other {# ملفات تم تحديثها}}",
|
"updatedFiles": "{count, plural, one {# ملف تم تحديثه} other {# ملفات تم تحديثها}}",
|
||||||
"openCommitWindowFailed": "فشل فتح نافذة الالتزام",
|
"openCommitWindowFailed": "فشل فتح نافذة الالتزام",
|
||||||
|
"openPushWindowFailed": "فشل فتح نافذة الدفع",
|
||||||
"upstreamSet": "تم تعيين فرع upstream",
|
"upstreamSet": "تم تعيين فرع upstream",
|
||||||
"upstreamSetAndPushed": "تم تعيين فرع upstream ودفع {count, plural, one {# التزام} other {# التزامات}}",
|
"upstreamSetAndPushed": "تم تعيين فرع upstream ودفع {count, plural, one {# التزام} other {# التزامات}}",
|
||||||
"noCommitsToPush": "لا توجد التزامات للدفع",
|
"noCommitsToPush": "لا توجد التزامات للدفع",
|
||||||
@@ -1032,6 +1033,23 @@
|
|||||||
"clickFileToDiff": "انقر اسم الملف لعرض الفرق",
|
"clickFileToDiff": "انقر اسم الملف لعرض الفرق",
|
||||||
"loadingDiff": "جارٍ تحميل diff..."
|
"loadingDiff": "جارٍ تحميل diff..."
|
||||||
},
|
},
|
||||||
|
"pushWindow": {
|
||||||
|
"title": "دفع الكود",
|
||||||
|
"noUnpushedCommits": "لا توجد التزامات غير مدفوعة",
|
||||||
|
"unpushed": "غير مدفوع",
|
||||||
|
"selectFileToViewDiff": "اختر ملفًا لعرض الفرق",
|
||||||
|
"before": "قبل",
|
||||||
|
"after": "بعد",
|
||||||
|
"push": "دفع",
|
||||||
|
"toasts": {
|
||||||
|
"pushSuccess": "تم الدفع بنجاح",
|
||||||
|
"pushFailed": "فشل الدفع",
|
||||||
|
"upstreamSet": "تم تعيين الفرع البعيد",
|
||||||
|
"upstreamSetAndPushed": "تم تعيين الفرع البعيد ودفع {count} التزام",
|
||||||
|
"noCommitsToPush": "لا توجد التزامات للدفع",
|
||||||
|
"pushedCommits": "تم دفع {count} التزام"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitLogTab": {
|
"gitLogTab": {
|
||||||
"filesTitle": "الملفات",
|
"filesTitle": "الملفات",
|
||||||
"expandAllFiles": "توسيع كل الملفات",
|
"expandAllFiles": "توسيع كل الملفات",
|
||||||
|
|||||||
@@ -882,6 +882,7 @@
|
|||||||
"allFilesUpToDate": "Alle Dateien sind aktuell",
|
"allFilesUpToDate": "Alle Dateien sind aktuell",
|
||||||
"updatedFiles": "{count, plural, one {# Datei aktualisiert} other {# Dateien aktualisiert}}",
|
"updatedFiles": "{count, plural, one {# Datei aktualisiert} other {# Dateien aktualisiert}}",
|
||||||
"openCommitWindowFailed": "Commit-Fenster konnte nicht geöffnet werden",
|
"openCommitWindowFailed": "Commit-Fenster konnte nicht geöffnet werden",
|
||||||
|
"openPushWindowFailed": "Push-Fenster konnte nicht geöffnet werden",
|
||||||
"upstreamSet": "Upstream-Branch wurde gesetzt",
|
"upstreamSet": "Upstream-Branch wurde gesetzt",
|
||||||
"upstreamSetAndPushed": "Upstream-Branch gesetzt und {count, plural, one {# Commit} other {# Commits}} gepusht",
|
"upstreamSetAndPushed": "Upstream-Branch gesetzt und {count, plural, one {# Commit} other {# Commits}} gepusht",
|
||||||
"noCommitsToPush": "Keine Commits zum Pushen",
|
"noCommitsToPush": "Keine Commits zum Pushen",
|
||||||
@@ -1032,6 +1033,23 @@
|
|||||||
"clickFileToDiff": "Dateinamen anklicken, um Diff zu sehen",
|
"clickFileToDiff": "Dateinamen anklicken, um Diff zu sehen",
|
||||||
"loadingDiff": "Diff wird geladen..."
|
"loadingDiff": "Diff wird geladen..."
|
||||||
},
|
},
|
||||||
|
"pushWindow": {
|
||||||
|
"title": "Code pushen",
|
||||||
|
"noUnpushedCommits": "Keine ungepushten Commits",
|
||||||
|
"unpushed": "Nicht gepusht",
|
||||||
|
"selectFileToViewDiff": "Datei auswählen, um Unterschiede anzuzeigen",
|
||||||
|
"before": "Vorher",
|
||||||
|
"after": "Nachher",
|
||||||
|
"push": "Pushen",
|
||||||
|
"toasts": {
|
||||||
|
"pushSuccess": "Push erfolgreich",
|
||||||
|
"pushFailed": "Push fehlgeschlagen",
|
||||||
|
"upstreamSet": "Remote-Tracking-Branch wurde eingerichtet",
|
||||||
|
"upstreamSetAndPushed": "Remote-Tracking-Branch eingerichtet und {count} Commits gepusht",
|
||||||
|
"noCommitsToPush": "Keine Commits zum Pushen",
|
||||||
|
"pushedCommits": "{count} Commits gepusht"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitLogTab": {
|
"gitLogTab": {
|
||||||
"filesTitle": "Dateien",
|
"filesTitle": "Dateien",
|
||||||
"expandAllFiles": "Alle Dateien ausklappen",
|
"expandAllFiles": "Alle Dateien ausklappen",
|
||||||
|
|||||||
@@ -882,6 +882,7 @@
|
|||||||
"allFilesUpToDate": "All files are up to date",
|
"allFilesUpToDate": "All files are up to date",
|
||||||
"updatedFiles": "Updated {count, plural, one {# file} other {# files}}",
|
"updatedFiles": "Updated {count, plural, one {# file} other {# files}}",
|
||||||
"openCommitWindowFailed": "Failed to open commit window",
|
"openCommitWindowFailed": "Failed to open commit window",
|
||||||
|
"openPushWindowFailed": "Failed to open push window",
|
||||||
"upstreamSet": "Upstream branch has been set",
|
"upstreamSet": "Upstream branch has been set",
|
||||||
"upstreamSetAndPushed": "Upstream branch set and pushed {count, plural, one {# commit} other {# commits}}",
|
"upstreamSetAndPushed": "Upstream branch set and pushed {count, plural, one {# commit} other {# commits}}",
|
||||||
"noCommitsToPush": "No commits to push",
|
"noCommitsToPush": "No commits to push",
|
||||||
@@ -1032,6 +1033,23 @@
|
|||||||
"clickFileToDiff": "Click a file name to view diff",
|
"clickFileToDiff": "Click a file name to view diff",
|
||||||
"loadingDiff": "Loading diff..."
|
"loadingDiff": "Loading diff..."
|
||||||
},
|
},
|
||||||
|
"pushWindow": {
|
||||||
|
"title": "Push Code",
|
||||||
|
"noUnpushedCommits": "No unpushed commits",
|
||||||
|
"unpushed": "Unpushed",
|
||||||
|
"selectFileToViewDiff": "Select a file to view diff",
|
||||||
|
"before": "Before",
|
||||||
|
"after": "After",
|
||||||
|
"push": "Push",
|
||||||
|
"toasts": {
|
||||||
|
"pushSuccess": "Push successful",
|
||||||
|
"pushFailed": "Push failed",
|
||||||
|
"upstreamSet": "Upstream branch has been set",
|
||||||
|
"upstreamSetAndPushed": "Upstream branch set and pushed {count, plural, one {# commit} other {# commits}}",
|
||||||
|
"noCommitsToPush": "No commits to push",
|
||||||
|
"pushedCommits": "Pushed {count, plural, one {# commit} other {# commits}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitLogTab": {
|
"gitLogTab": {
|
||||||
"filesTitle": "Files",
|
"filesTitle": "Files",
|
||||||
"expandAllFiles": "Expand all files",
|
"expandAllFiles": "Expand all files",
|
||||||
|
|||||||
@@ -882,6 +882,7 @@
|
|||||||
"allFilesUpToDate": "Todos los archivos están actualizados",
|
"allFilesUpToDate": "Todos los archivos están actualizados",
|
||||||
"updatedFiles": "{count, plural, one {# archivo actualizado} other {# archivos actualizados}}",
|
"updatedFiles": "{count, plural, one {# archivo actualizado} other {# archivos actualizados}}",
|
||||||
"openCommitWindowFailed": "No se pudo abrir la ventana de commit",
|
"openCommitWindowFailed": "No se pudo abrir la ventana de commit",
|
||||||
|
"openPushWindowFailed": "Error al abrir la ventana de envío",
|
||||||
"upstreamSet": "La rama upstream se ha configurado",
|
"upstreamSet": "La rama upstream se ha configurado",
|
||||||
"upstreamSetAndPushed": "Rama upstream configurada y se enviaron {count, plural, one {# commit} other {# commits}}",
|
"upstreamSetAndPushed": "Rama upstream configurada y se enviaron {count, plural, one {# commit} other {# commits}}",
|
||||||
"noCommitsToPush": "No hay commits para enviar",
|
"noCommitsToPush": "No hay commits para enviar",
|
||||||
@@ -1032,6 +1033,23 @@
|
|||||||
"clickFileToDiff": "Haz clic en un nombre de archivo para ver diff",
|
"clickFileToDiff": "Haz clic en un nombre de archivo para ver diff",
|
||||||
"loadingDiff": "Cargando diff..."
|
"loadingDiff": "Cargando diff..."
|
||||||
},
|
},
|
||||||
|
"pushWindow": {
|
||||||
|
"title": "Enviar código",
|
||||||
|
"noUnpushedCommits": "No hay commits sin enviar",
|
||||||
|
"unpushed": "Sin enviar",
|
||||||
|
"selectFileToViewDiff": "Selecciona un archivo para ver las diferencias",
|
||||||
|
"before": "Antes",
|
||||||
|
"after": "Después",
|
||||||
|
"push": "Enviar",
|
||||||
|
"toasts": {
|
||||||
|
"pushSuccess": "Envío exitoso",
|
||||||
|
"pushFailed": "Error al enviar",
|
||||||
|
"upstreamSet": "Se ha configurado la rama remota",
|
||||||
|
"upstreamSetAndPushed": "Rama remota configurada y enviados {count} commits",
|
||||||
|
"noCommitsToPush": "No hay commits para enviar",
|
||||||
|
"pushedCommits": "Enviados {count} commits"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitLogTab": {
|
"gitLogTab": {
|
||||||
"filesTitle": "Archivos",
|
"filesTitle": "Archivos",
|
||||||
"expandAllFiles": "Expandir todos los archivos",
|
"expandAllFiles": "Expandir todos los archivos",
|
||||||
|
|||||||
@@ -882,6 +882,7 @@
|
|||||||
"allFilesUpToDate": "Tous les fichiers sont à jour",
|
"allFilesUpToDate": "Tous les fichiers sont à jour",
|
||||||
"updatedFiles": "{count, plural, one {# fichier mis à jour} other {# fichiers mis à jour}}",
|
"updatedFiles": "{count, plural, one {# fichier mis à jour} other {# fichiers mis à jour}}",
|
||||||
"openCommitWindowFailed": "Impossible d’ouvrir la fenêtre de commit",
|
"openCommitWindowFailed": "Impossible d’ouvrir la fenêtre de commit",
|
||||||
|
"openPushWindowFailed": "Impossible d’ouvrir la fenêtre de push",
|
||||||
"upstreamSet": "La branche upstream a été définie",
|
"upstreamSet": "La branche upstream a été définie",
|
||||||
"upstreamSetAndPushed": "Branche upstream définie et {count, plural, one {# commit} other {# commits}} poussé(s)",
|
"upstreamSetAndPushed": "Branche upstream définie et {count, plural, one {# commit} other {# commits}} poussé(s)",
|
||||||
"noCommitsToPush": "Aucun commit à pousser",
|
"noCommitsToPush": "Aucun commit à pousser",
|
||||||
@@ -1032,6 +1033,23 @@
|
|||||||
"clickFileToDiff": "Cliquez sur un nom de fichier pour voir le diff",
|
"clickFileToDiff": "Cliquez sur un nom de fichier pour voir le diff",
|
||||||
"loadingDiff": "Chargement du diff..."
|
"loadingDiff": "Chargement du diff..."
|
||||||
},
|
},
|
||||||
|
"pushWindow": {
|
||||||
|
"title": "Pousser le code",
|
||||||
|
"noUnpushedCommits": "Aucun commit non poussé",
|
||||||
|
"unpushed": "Non poussé",
|
||||||
|
"selectFileToViewDiff": "Sélectionnez un fichier pour voir les différences",
|
||||||
|
"before": "Avant",
|
||||||
|
"after": "Après",
|
||||||
|
"push": "Pousser",
|
||||||
|
"toasts": {
|
||||||
|
"pushSuccess": "Push réussi",
|
||||||
|
"pushFailed": "Échec du push",
|
||||||
|
"upstreamSet": "La branche distante a été configurée",
|
||||||
|
"upstreamSetAndPushed": "Branche distante configurée et {count} commits poussés",
|
||||||
|
"noCommitsToPush": "Aucun commit à pousser",
|
||||||
|
"pushedCommits": "{count} commits poussés"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitLogTab": {
|
"gitLogTab": {
|
||||||
"filesTitle": "Fichiers",
|
"filesTitle": "Fichiers",
|
||||||
"expandAllFiles": "Développer tous les fichiers",
|
"expandAllFiles": "Développer tous les fichiers",
|
||||||
|
|||||||
@@ -882,6 +882,7 @@
|
|||||||
"allFilesUpToDate": "すべてのファイルは最新です",
|
"allFilesUpToDate": "すべてのファイルは最新です",
|
||||||
"updatedFiles": "{count, plural, one {# 個のファイルを更新} other {# 個のファイルを更新}}",
|
"updatedFiles": "{count, plural, one {# 個のファイルを更新} other {# 個のファイルを更新}}",
|
||||||
"openCommitWindowFailed": "コミットウィンドウを開けませんでした",
|
"openCommitWindowFailed": "コミットウィンドウを開けませんでした",
|
||||||
|
"openPushWindowFailed": "プッシュウィンドウを開けませんでした",
|
||||||
"upstreamSet": "アップストリームブランチを設定しました",
|
"upstreamSet": "アップストリームブランチを設定しました",
|
||||||
"upstreamSetAndPushed": "アップストリームブランチを設定し、{count, plural, one {# 件のコミット} other {# 件のコミット}}をプッシュしました",
|
"upstreamSetAndPushed": "アップストリームブランチを設定し、{count, plural, one {# 件のコミット} other {# 件のコミット}}をプッシュしました",
|
||||||
"noCommitsToPush": "プッシュするコミットはありません",
|
"noCommitsToPush": "プッシュするコミットはありません",
|
||||||
@@ -1032,6 +1033,23 @@
|
|||||||
"clickFileToDiff": "ファイル名をクリックして差分を表示",
|
"clickFileToDiff": "ファイル名をクリックして差分を表示",
|
||||||
"loadingDiff": "差分を読み込み中..."
|
"loadingDiff": "差分を読み込み中..."
|
||||||
},
|
},
|
||||||
|
"pushWindow": {
|
||||||
|
"title": "コードをプッシュ",
|
||||||
|
"noUnpushedCommits": "未プッシュのコミットはありません",
|
||||||
|
"unpushed": "未プッシュ",
|
||||||
|
"selectFileToViewDiff": "ファイルを選択して差分を表示",
|
||||||
|
"before": "変更前",
|
||||||
|
"after": "変更後",
|
||||||
|
"push": "プッシュ",
|
||||||
|
"toasts": {
|
||||||
|
"pushSuccess": "プッシュ成功",
|
||||||
|
"pushFailed": "プッシュ失敗",
|
||||||
|
"upstreamSet": "リモート追跡ブランチが設定されました",
|
||||||
|
"upstreamSetAndPushed": "リモート追跡ブランチを設定し、{count}件のコミットをプッシュしました",
|
||||||
|
"noCommitsToPush": "プッシュするコミットはありません",
|
||||||
|
"pushedCommits": "{count}件のコミットをプッシュしました"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitLogTab": {
|
"gitLogTab": {
|
||||||
"filesTitle": "ファイル",
|
"filesTitle": "ファイル",
|
||||||
"expandAllFiles": "すべてのファイルを展開",
|
"expandAllFiles": "すべてのファイルを展開",
|
||||||
|
|||||||
@@ -882,6 +882,7 @@
|
|||||||
"allFilesUpToDate": "모든 파일이 최신 상태입니다",
|
"allFilesUpToDate": "모든 파일이 최신 상태입니다",
|
||||||
"updatedFiles": "{count, plural, one {#개 파일 업데이트됨} other {#개 파일 업데이트됨}}",
|
"updatedFiles": "{count, plural, one {#개 파일 업데이트됨} other {#개 파일 업데이트됨}}",
|
||||||
"openCommitWindowFailed": "커밋 창을 열지 못했습니다",
|
"openCommitWindowFailed": "커밋 창을 열지 못했습니다",
|
||||||
|
"openPushWindowFailed": "푸시 창 열기 실패",
|
||||||
"upstreamSet": "업스트림 브랜치가 설정되었습니다",
|
"upstreamSet": "업스트림 브랜치가 설정되었습니다",
|
||||||
"upstreamSetAndPushed": "업스트림 브랜치를 설정하고 {count, plural, one {#개 커밋} other {#개 커밋}}을 푸시했습니다",
|
"upstreamSetAndPushed": "업스트림 브랜치를 설정하고 {count, plural, one {#개 커밋} other {#개 커밋}}을 푸시했습니다",
|
||||||
"noCommitsToPush": "푸시할 커밋이 없습니다",
|
"noCommitsToPush": "푸시할 커밋이 없습니다",
|
||||||
@@ -1032,6 +1033,23 @@
|
|||||||
"clickFileToDiff": "파일 이름을 클릭해 diff를 확인하세요",
|
"clickFileToDiff": "파일 이름을 클릭해 diff를 확인하세요",
|
||||||
"loadingDiff": "diff 로딩 중..."
|
"loadingDiff": "diff 로딩 중..."
|
||||||
},
|
},
|
||||||
|
"pushWindow": {
|
||||||
|
"title": "코드 푸시",
|
||||||
|
"noUnpushedCommits": "푸시되지 않은 커밋이 없습니다",
|
||||||
|
"unpushed": "미푸시",
|
||||||
|
"selectFileToViewDiff": "파일을 선택하여 차이 보기",
|
||||||
|
"before": "변경 전",
|
||||||
|
"after": "변경 후",
|
||||||
|
"push": "푸시",
|
||||||
|
"toasts": {
|
||||||
|
"pushSuccess": "푸시 성공",
|
||||||
|
"pushFailed": "푸시 실패",
|
||||||
|
"upstreamSet": "원격 추적 브랜치가 설정되었습니다",
|
||||||
|
"upstreamSetAndPushed": "원격 추적 브랜치 설정 및 {count}개 커밋 푸시 완료",
|
||||||
|
"noCommitsToPush": "푸시할 커밋이 없습니다",
|
||||||
|
"pushedCommits": "{count}개 커밋 푸시 완료"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitLogTab": {
|
"gitLogTab": {
|
||||||
"filesTitle": "파일",
|
"filesTitle": "파일",
|
||||||
"expandAllFiles": "모든 파일 펼치기",
|
"expandAllFiles": "모든 파일 펼치기",
|
||||||
|
|||||||
@@ -882,6 +882,7 @@
|
|||||||
"allFilesUpToDate": "Todos os arquivos estão atualizados",
|
"allFilesUpToDate": "Todos os arquivos estão atualizados",
|
||||||
"updatedFiles": "{count, plural, one {# arquivo atualizado} other {# arquivos atualizados}}",
|
"updatedFiles": "{count, plural, one {# arquivo atualizado} other {# arquivos atualizados}}",
|
||||||
"openCommitWindowFailed": "Falha ao abrir a janela de commit",
|
"openCommitWindowFailed": "Falha ao abrir a janela de commit",
|
||||||
|
"openPushWindowFailed": "Falha ao abrir janela de envio",
|
||||||
"upstreamSet": "A branch upstream foi definida",
|
"upstreamSet": "A branch upstream foi definida",
|
||||||
"upstreamSetAndPushed": "Branch upstream definida e {count, plural, one {# commit} other {# commits}} enviado(s)",
|
"upstreamSetAndPushed": "Branch upstream definida e {count, plural, one {# commit} other {# commits}} enviado(s)",
|
||||||
"noCommitsToPush": "Não há commits para enviar",
|
"noCommitsToPush": "Não há commits para enviar",
|
||||||
@@ -1032,6 +1033,23 @@
|
|||||||
"clickFileToDiff": "Clique no nome do arquivo para ver o diff",
|
"clickFileToDiff": "Clique no nome do arquivo para ver o diff",
|
||||||
"loadingDiff": "Carregando diff..."
|
"loadingDiff": "Carregando diff..."
|
||||||
},
|
},
|
||||||
|
"pushWindow": {
|
||||||
|
"title": "Enviar código",
|
||||||
|
"noUnpushedCommits": "Nenhum commit não enviado",
|
||||||
|
"unpushed": "Não enviado",
|
||||||
|
"selectFileToViewDiff": "Selecione um arquivo para ver as diferenças",
|
||||||
|
"before": "Antes",
|
||||||
|
"after": "Depois",
|
||||||
|
"push": "Enviar",
|
||||||
|
"toasts": {
|
||||||
|
"pushSuccess": "Envio bem-sucedido",
|
||||||
|
"pushFailed": "Falha no envio",
|
||||||
|
"upstreamSet": "Branch remoto foi configurado",
|
||||||
|
"upstreamSetAndPushed": "Branch remoto configurado e {count} commits enviados",
|
||||||
|
"noCommitsToPush": "Nenhum commit para enviar",
|
||||||
|
"pushedCommits": "{count} commits enviados"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitLogTab": {
|
"gitLogTab": {
|
||||||
"filesTitle": "Arquivos",
|
"filesTitle": "Arquivos",
|
||||||
"expandAllFiles": "Expandir todos os arquivos",
|
"expandAllFiles": "Expandir todos os arquivos",
|
||||||
|
|||||||
@@ -882,6 +882,7 @@
|
|||||||
"allFilesUpToDate": "所有文件均为最新版本",
|
"allFilesUpToDate": "所有文件均为最新版本",
|
||||||
"updatedFiles": "已更新 {count} 个文件",
|
"updatedFiles": "已更新 {count} 个文件",
|
||||||
"openCommitWindowFailed": "打开提交窗口失败",
|
"openCommitWindowFailed": "打开提交窗口失败",
|
||||||
|
"openPushWindowFailed": "打开推送窗口失败",
|
||||||
"upstreamSet": "已设置远程跟踪分支",
|
"upstreamSet": "已设置远程跟踪分支",
|
||||||
"upstreamSetAndPushed": "已设置远程跟踪分支并推送 {count} 个提交",
|
"upstreamSetAndPushed": "已设置远程跟踪分支并推送 {count} 个提交",
|
||||||
"noCommitsToPush": "没有可推送的提交",
|
"noCommitsToPush": "没有可推送的提交",
|
||||||
@@ -1032,6 +1033,23 @@
|
|||||||
"clickFileToDiff": "点击文件名查看差异",
|
"clickFileToDiff": "点击文件名查看差异",
|
||||||
"loadingDiff": "加载差异..."
|
"loadingDiff": "加载差异..."
|
||||||
},
|
},
|
||||||
|
"pushWindow": {
|
||||||
|
"title": "推送代码",
|
||||||
|
"noUnpushedCommits": "没有未推送的提交",
|
||||||
|
"unpushed": "未推送",
|
||||||
|
"selectFileToViewDiff": "选择文件查看差异",
|
||||||
|
"before": "修改前",
|
||||||
|
"after": "修改后",
|
||||||
|
"push": "推送",
|
||||||
|
"toasts": {
|
||||||
|
"pushSuccess": "推送成功",
|
||||||
|
"pushFailed": "推送失败",
|
||||||
|
"upstreamSet": "已设置远程跟踪分支",
|
||||||
|
"upstreamSetAndPushed": "已设置远程跟踪分支并推送 {count} 个提交",
|
||||||
|
"noCommitsToPush": "没有可推送的提交",
|
||||||
|
"pushedCommits": "已推送 {count} 个提交"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitLogTab": {
|
"gitLogTab": {
|
||||||
"filesTitle": "文件",
|
"filesTitle": "文件",
|
||||||
"expandAllFiles": "展开全部文件",
|
"expandAllFiles": "展开全部文件",
|
||||||
|
|||||||
@@ -882,6 +882,7 @@
|
|||||||
"allFilesUpToDate": "所有檔案均為最新版本",
|
"allFilesUpToDate": "所有檔案均為最新版本",
|
||||||
"updatedFiles": "已更新 {count} 個檔案",
|
"updatedFiles": "已更新 {count} 個檔案",
|
||||||
"openCommitWindowFailed": "打開提交視窗失敗",
|
"openCommitWindowFailed": "打開提交視窗失敗",
|
||||||
|
"openPushWindowFailed": "開啟推送視窗失敗",
|
||||||
"upstreamSet": "已設定遠端追蹤分支",
|
"upstreamSet": "已設定遠端追蹤分支",
|
||||||
"upstreamSetAndPushed": "已設定遠端追蹤分支並推送 {count} 個提交",
|
"upstreamSetAndPushed": "已設定遠端追蹤分支並推送 {count} 個提交",
|
||||||
"noCommitsToPush": "沒有可推送的提交",
|
"noCommitsToPush": "沒有可推送的提交",
|
||||||
@@ -1032,6 +1033,23 @@
|
|||||||
"clickFileToDiff": "點擊檔案名稱查看差異",
|
"clickFileToDiff": "點擊檔案名稱查看差異",
|
||||||
"loadingDiff": "載入差異中..."
|
"loadingDiff": "載入差異中..."
|
||||||
},
|
},
|
||||||
|
"pushWindow": {
|
||||||
|
"title": "推送程式碼",
|
||||||
|
"noUnpushedCommits": "沒有未推送的提交",
|
||||||
|
"unpushed": "未推送",
|
||||||
|
"selectFileToViewDiff": "選擇檔案查看差異",
|
||||||
|
"before": "修改前",
|
||||||
|
"after": "修改後",
|
||||||
|
"push": "推送",
|
||||||
|
"toasts": {
|
||||||
|
"pushSuccess": "推送成功",
|
||||||
|
"pushFailed": "推送失敗",
|
||||||
|
"upstreamSet": "已設定遠端追蹤分支",
|
||||||
|
"upstreamSetAndPushed": "已設定遠端追蹤分支並推送 {count} 個提交",
|
||||||
|
"noCommitsToPush": "沒有可推送的提交",
|
||||||
|
"pushedCommits": "已推送 {count} 個提交"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gitLogTab": {
|
"gitLogTab": {
|
||||||
"filesTitle": "檔案",
|
"filesTitle": "檔案",
|
||||||
"expandAllFiles": "展開全部檔案",
|
"expandAllFiles": "展開全部檔案",
|
||||||
|
|||||||
@@ -661,6 +661,10 @@ export async function openStashWindow(folderId: number): Promise<void> {
|
|||||||
return invoke("open_stash_window", { folderId })
|
return invoke("open_stash_window", { folderId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function openPushWindow(folderId: number): Promise<void> {
|
||||||
|
return invoke("open_push_window", { folderId })
|
||||||
|
}
|
||||||
|
|
||||||
export async function gitStashPush(
|
export async function gitStashPush(
|
||||||
path: string,
|
path: string,
|
||||||
message?: string,
|
message?: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user