feat(git): 工作区 Git 同步——侧栏同步条、远程设置、定时同步(提交→变基拉取→推送,冲突停下不覆盖)(docs/27)
This commit is contained in:
129
src/App.tsx
129
src/App.tsx
@@ -29,10 +29,13 @@ import {
|
||||
pickImageFile, pickAnyFile, pickAndReadWebImage, copyToAssets, saveBytesToAssets, fileToBase64,
|
||||
statFile, createTextFile, createDirPath, renamePath, deleteToTrash, revealInExplorer, type DirEntry, type RecoveryEntry,
|
||||
pickSaveHtmlFile, pickImportFile, readFileBase64, readBinaryFile, pickAndReadWebImport,
|
||||
gitStatus, gitInitRepo, gitSetRemote, gitSyncNow, type GitStatusInfo,
|
||||
} from './services/platform'
|
||||
import * as store from './services/store'
|
||||
import { recoveryKey, planRecovery } from './utils/recovery'
|
||||
import { appWindowClaims } from './services/windowClaims'
|
||||
import { appWindowClaims, appWindowId } from './services/windowClaims'
|
||||
import { acquireGitSyncLease, gitStatusLabel, gitSyncCommitMessage, loadGitSyncSettings, releaseGitSyncLease, saveGitSyncSettings, type GitSyncSettings } from './services/gitSync'
|
||||
import { GitSyncModal } from './components/GitSyncModal'
|
||||
import { RecoveryModal, ConflictModal, QuitModal, PromptModal, ConfirmModal } from './components/Stage4Modals'
|
||||
import { AppearanceModal, ExportModal } from './components/Stage5Modals'
|
||||
import { DOC_THEMES, docThemeById } from './themes'
|
||||
@@ -78,6 +81,7 @@ export default function App() {
|
||||
const tab = activeTab(state)
|
||||
|
||||
const [workspaceRoot, setWorkspaceRoot] = useState<string | null>(null)
|
||||
|
||||
const [sidebarMode, setSidebarMode] = useState<'files' | 'outline' | 'search'>('outline')
|
||||
const [recent, setRecent] = useState<string[]>(loadRecent)
|
||||
const [cursor, setCursor] = useState<CursorInfo>({ line: 1, col: 1 })
|
||||
@@ -235,6 +239,106 @@ export default function App() {
|
||||
|
||||
const tauriMode = isTauri()
|
||||
|
||||
// ---- Git 同步(工作区文件夹) ----
|
||||
const [gitInfo, setGitInfo] = useState<GitStatusInfo | null>(null)
|
||||
const [gitCfg, setGitCfg] = useState<GitSyncSettings>({ intervalMin: 0 })
|
||||
const [gitSyncing, setGitSyncing] = useState(false)
|
||||
const [gitMsg, setGitMsg] = useState<{ text: string; isError: boolean } | null>(null)
|
||||
const [gitSettingsOpen, setGitSettingsOpen] = useState(false)
|
||||
const [gitSettingsError, setGitSettingsError] = useState<string | null>(null)
|
||||
const [gitSettingsBusy, setGitSettingsBusy] = useState(false)
|
||||
const gitSyncingRef = useRef(false)
|
||||
const gitRootRef = useRef<string | null>(null)
|
||||
|
||||
const refreshGitStatus = useCallback(async (root: string) => {
|
||||
try {
|
||||
const info = await gitStatus(root)
|
||||
if (gitRootRef.current === root) setGitInfo(info)
|
||||
} catch {
|
||||
if (gitRootRef.current === root) setGitInfo(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const runGitSync = useCallback(async (auto: boolean) => {
|
||||
const root = gitRootRef.current
|
||||
if (!root || gitSyncingRef.current) return
|
||||
if (auto && !acquireGitSyncLease(root, appWindowId)) return
|
||||
gitSyncingRef.current = true
|
||||
setGitSyncing(true)
|
||||
try {
|
||||
const outcome = await gitSyncNow(root, gitSyncCommitMessage(auto))
|
||||
if (gitRootRef.current !== root) return
|
||||
setGitMsg({ text: outcome.summary, isError: false })
|
||||
if (outcome.pulled) setTreeRefresh((n) => n + 1)
|
||||
} catch (e) {
|
||||
if (gitRootRef.current === root) setGitMsg({ text: String(e), isError: true })
|
||||
} finally {
|
||||
gitSyncingRef.current = false
|
||||
setGitSyncing(false)
|
||||
if (gitRootRef.current === root) void refreshGitStatus(root)
|
||||
}
|
||||
}, [refreshGitStatus])
|
||||
const gitSyncRunnerRef = useRef(runGitSync)
|
||||
gitSyncRunnerRef.current = runGitSync
|
||||
|
||||
// 工作区切换:重读该工作区的 Git 状态与同步设置
|
||||
useEffect(() => {
|
||||
gitRootRef.current = workspaceRoot
|
||||
setGitInfo(null)
|
||||
setGitMsg(null)
|
||||
setGitCfg({ intervalMin: 0 })
|
||||
setGitSettingsOpen(false)
|
||||
if (!workspaceRoot || !tauriMode) return
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
const cfg = await loadGitSyncSettings(workspaceRoot)
|
||||
if (!cancelled && gitRootRef.current === workspaceRoot) setGitCfg(cfg)
|
||||
})()
|
||||
void refreshGitStatus(workspaceRoot)
|
||||
return () => { cancelled = true }
|
||||
}, [workspaceRoot, tauriMode, refreshGitStatus])
|
||||
|
||||
// 定时同步:到点自动跑一次(租约保证多窗口只有一个执行);开着工作区时先续租
|
||||
useEffect(() => {
|
||||
if (!workspaceRoot || !tauriMode || gitCfg.intervalMin <= 0 || !gitInfo?.isRepo) return
|
||||
const root = workspaceRoot
|
||||
acquireGitSyncLease(root, appWindowId)
|
||||
const leaseTimer = setInterval(() => acquireGitSyncLease(root, appWindowId), 10_000)
|
||||
const first = setTimeout(() => { void gitSyncRunnerRef.current(true) }, 5_000)
|
||||
const timer = setInterval(() => { void gitSyncRunnerRef.current(true) }, gitCfg.intervalMin * 60_000)
|
||||
return () => {
|
||||
clearInterval(leaseTimer)
|
||||
clearTimeout(first)
|
||||
clearInterval(timer)
|
||||
releaseGitSyncLease(root, appWindowId)
|
||||
}
|
||||
}, [workspaceRoot, tauriMode, gitCfg.intervalMin, gitInfo?.isRepo])
|
||||
|
||||
const saveGitSettings = useCallback(async (remoteUrl: string, branch: string, intervalMin: number) => {
|
||||
const root = gitRootRef.current
|
||||
if (!root) return
|
||||
setGitSettingsBusy(true)
|
||||
setGitSettingsError(null)
|
||||
try {
|
||||
let info: GitStatusInfo | null = null
|
||||
if (!gitInfo?.isRepo) {
|
||||
info = await gitInitRepo(root, remoteUrl, branch)
|
||||
} else if (remoteUrl !== (gitInfo.remoteUrl ?? '')) {
|
||||
info = await gitSetRemote(root, remoteUrl)
|
||||
}
|
||||
await saveGitSyncSettings(root, { intervalMin })
|
||||
setGitCfg({ intervalMin })
|
||||
if (info) setGitInfo(info)
|
||||
else void refreshGitStatus(root)
|
||||
setGitSettingsOpen(false)
|
||||
setGitMsg({ text: info && !gitInfo?.isRepo ? '已初始化仓库并保存设置' : 'Git 同步设置已保存', isError: false })
|
||||
} catch (e) {
|
||||
setGitSettingsError(String(e))
|
||||
} finally {
|
||||
setGitSettingsBusy(false)
|
||||
}
|
||||
}, [gitInfo, refreshGitStatus])
|
||||
|
||||
const uiSaveTimerRef = useRef<number | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
if (!uiLoadedRef.current) return
|
||||
@@ -1236,7 +1340,7 @@ export default function App() {
|
||||
|
||||
anyModalRef.current = Boolean(
|
||||
modal || appearanceOpen || exportOpen || pendingClose || conflict
|
||||
|| promptSpec || confirmSpec || quitOpen || (recoveryOffer.length > 0 && !recoveryDismissed),
|
||||
|| promptSpec || confirmSpec || quitOpen || gitSettingsOpen || (recoveryOffer.length > 0 && !recoveryDismissed),
|
||||
)
|
||||
findOpenRef.current = findUi.open
|
||||
|
||||
@@ -1390,6 +1494,15 @@ export default function App() {
|
||||
onJumpHeading={jumpHeading}
|
||||
onOpenSearchResult={openSearchResult}
|
||||
tauriMode={tauriMode}
|
||||
gitSync={tauriMode && workspaceRoot ? {
|
||||
isRepo: Boolean(gitInfo?.isRepo),
|
||||
label: gitStatusLabel(gitInfo, gitSyncing),
|
||||
syncing: gitSyncing,
|
||||
lastMessage: gitMsg?.text ?? null,
|
||||
lastIsError: gitMsg?.isError ?? false,
|
||||
onSyncNow: () => { void runGitSync(false) },
|
||||
onOpenSettings: () => { setGitSettingsError(null); setGitSettingsOpen(true) },
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -1552,6 +1665,18 @@ export default function App() {
|
||||
tauriMode={tauriMode}
|
||||
zoom={zoom}
|
||||
/>
|
||||
{gitSettingsOpen && workspaceRoot && (
|
||||
<GitSyncModal
|
||||
isRepo={Boolean(gitInfo?.isRepo)}
|
||||
branch={gitInfo?.branch ?? null}
|
||||
initialRemoteUrl={gitInfo?.remoteUrl ?? ''}
|
||||
initialIntervalMin={gitCfg.intervalMin}
|
||||
busy={gitSettingsBusy}
|
||||
error={gitSettingsError}
|
||||
onSave={(remoteUrl, branch, intervalMin) => { void saveGitSettings(remoteUrl, branch, intervalMin) }}
|
||||
onClose={() => setGitSettingsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{appearanceOpen && (
|
||||
<AppearanceModal
|
||||
docTheme={ui.docTheme}
|
||||
|
||||
80
src/components/GitSyncModal.tsx
Normal file
80
src/components/GitSyncModal.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
// Git 同步设置:远程仓库地址、(初始化时的)分支、定时同步间隔。
|
||||
// 工作区还不是仓库时,保存=初始化仓库并绑定远程;已是仓库时只更新设置。
|
||||
import { useState } from 'react'
|
||||
import { Dropdown } from './Dropdown'
|
||||
|
||||
const INTERVAL_OPTIONS = [
|
||||
{ value: '0', label: '关闭(只手动同步)' },
|
||||
{ value: '5', label: '每 5 分钟' },
|
||||
{ value: '15', label: '每 15 分钟' },
|
||||
{ value: '30', label: '每 30 分钟' },
|
||||
{ value: '60', label: '每小时' },
|
||||
]
|
||||
|
||||
export function GitSyncModal({ isRepo, branch, initialRemoteUrl, initialIntervalMin, busy, error, onSave, onClose }: {
|
||||
isRepo: boolean
|
||||
branch: string | null
|
||||
initialRemoteUrl: string
|
||||
initialIntervalMin: number
|
||||
busy: boolean
|
||||
error: string | null
|
||||
onSave: (remoteUrl: string, branch: string, intervalMin: number) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [remoteUrl, setRemoteUrl] = useState(initialRemoteUrl)
|
||||
const [branchInput, setBranchInput] = useState('main')
|
||||
const [intervalMin, setIntervalMin] = useState(String(initialIntervalMin))
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop">
|
||||
<div className="modal">
|
||||
<h3>Git 同步设置</h3>
|
||||
<p className="modal-note">
|
||||
同步顺序:提交本地更改 → 拉取远程更新(变基)→ 推送。若本地与远程改了相同内容,
|
||||
同步会停下并提示,不会强行覆盖,需要在该文件夹手动处理。
|
||||
</p>
|
||||
<label className="modal-field">
|
||||
<span>远程仓库地址(origin)</span>
|
||||
<input
|
||||
className="input"
|
||||
value={remoteUrl}
|
||||
placeholder="https://git.xhome.cc/xieyinlu/notes.git"
|
||||
onChange={(e) => setRemoteUrl(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
{isRepo ? (
|
||||
<p className="modal-note">当前分支:{branch ?? '(无)'}。分支切换请用 Git 工具完成,这里只管同步。</p>
|
||||
) : (
|
||||
<label className="modal-field">
|
||||
<span>初始分支</span>
|
||||
<input
|
||||
className="input"
|
||||
value={branchInput}
|
||||
onChange={(e) => setBranchInput(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<label className="modal-field">
|
||||
<span>定时同步</span>
|
||||
<Dropdown
|
||||
value={intervalMin}
|
||||
options={INTERVAL_OPTIONS}
|
||||
onChange={setIntervalMin}
|
||||
ariaLabel="定时同步间隔"
|
||||
/>
|
||||
</label>
|
||||
{error && <p className="modal-error">{error}</p>}
|
||||
<div className="modal-actions">
|
||||
<button onClick={onClose} disabled={busy}>取消</button>
|
||||
<button
|
||||
className="primary"
|
||||
disabled={busy}
|
||||
onClick={() => onSave(remoteUrl.trim(), branchInput.trim() || 'main', Number(intervalMin) || 0)}
|
||||
>
|
||||
{busy ? '处理中…' : isRepo ? '保存设置' : '初始化并启用'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,17 @@ import { isPathInside, parentDirOf, pathsEqual } from '../utils/text'
|
||||
import type { OutlineItem } from '../utils/text'
|
||||
import { baseName } from '../utils/text'
|
||||
|
||||
/** Git 同步条所需的展示状态与动作(由 App 持有真实状态)。 */
|
||||
export interface GitSyncUi {
|
||||
isRepo: boolean
|
||||
label: string
|
||||
syncing: boolean
|
||||
lastMessage: string | null
|
||||
lastIsError: boolean
|
||||
onSyncNow: () => void
|
||||
onOpenSettings: () => void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
mode: 'files' | 'outline' | 'search'
|
||||
onModeChange: (mode: 'files' | 'outline' | 'search') => void
|
||||
@@ -33,6 +44,8 @@ interface Props {
|
||||
outline: OutlineItem[]
|
||||
onJumpHeading: (item: OutlineItem) => void
|
||||
tauriMode: boolean
|
||||
/** Git 同步条(仅桌面模式且已打开文件夹时由 App 传入) */
|
||||
gitSync?: GitSyncUi
|
||||
}
|
||||
|
||||
function Sidebar(p: Props) {
|
||||
@@ -67,6 +80,8 @@ function Sidebar(p: Props) {
|
||||
))}
|
||||
</div>
|
||||
) : p.workspaceRoot ? (
|
||||
<>
|
||||
{p.gitSync && <GitSyncBar ui={p.gitSync} />}
|
||||
<FileTree
|
||||
onOpenInNewWindowEntry={p.onOpenInNewWindowEntry}
|
||||
onSaveAsEntry={p.onSaveAsEntry}
|
||||
@@ -83,6 +98,7 @@ function Sidebar(p: Props) {
|
||||
onRevealEntry={p.onRevealEntry}
|
||||
refreshSignal={p.refreshSignal ?? 0}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="sidebar-empty center">
|
||||
<p>尚未打开文件夹。</p>
|
||||
@@ -106,6 +122,32 @@ interface TreeDnd {
|
||||
drop: (dir: string) => void
|
||||
}
|
||||
|
||||
function GitSyncBar({ ui }: { ui: GitSyncUi }) {
|
||||
return (
|
||||
<div className="gitsync">
|
||||
<div className="gitsync-bar">
|
||||
<span className="gitsync-title">Git 同步</span>
|
||||
<span className="gitsync-status">{ui.label}</span>
|
||||
<span className="gitsync-actions">
|
||||
{ui.isRepo && (
|
||||
<Tooltip label="立即同步">
|
||||
<button className="gitsync-btn" aria-label="立即同步" disabled={ui.syncing} onClick={ui.onSyncNow}>
|
||||
<svg className={ui.syncing ? 'ws-spin' : ''} width="14" height="14" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 12a8 8 0 1 1-2.34-5.66" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /><path d="M20.2 3.6v4h-4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" /></svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label="Git 同步设置">
|
||||
<button className="gitsync-btn" aria-label="Git 同步设置" onClick={ui.onOpenSettings}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8h10M18 8h2M4 16h4M12 16h8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /><circle cx="16" cy="8" r="2.2" fill="none" stroke="currentColor" strokeWidth="1.8" /><circle cx="10" cy="16" r="2.2" fill="none" stroke="currentColor" strokeWidth="1.8" /></svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
{ui.lastMessage && <div className={`gitsync-msg${ui.lastIsError ? ' error' : ''}`}>{ui.lastMessage}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FileTree({ root, activePath, onOpenFile, onCreateFileIn, onCreateFolderIn, onRenameEntry, onDeleteEntry, refreshSignal, onOpenInNewWindowEntry, onSaveAsEntry, onExportEntry, onPrintEntry, onMoveEntry, onRevealEntry }: {
|
||||
root: string
|
||||
activePath: string | null
|
||||
|
||||
97
src/services/gitSync.ts
Normal file
97
src/services/gitSync.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// Git 同步的设置与文案:设置按工作区存进偏好(key=gitsync:<root>),
|
||||
// 定时同步用 localStorage 租约保证多窗口只有一个执行(与草稿认领同一思路)。
|
||||
|
||||
import { dbPrefGet, dbPrefSet, type GitStatusInfo } from './platform'
|
||||
|
||||
export interface GitSyncSettings {
|
||||
/** 定时同步间隔(分钟);0=只手动 */
|
||||
intervalMin: number
|
||||
}
|
||||
|
||||
export const DEFAULT_GIT_SYNC_SETTINGS: GitSyncSettings = { intervalMin: 0 }
|
||||
|
||||
export function parseGitSyncSettings(raw: string | null): GitSyncSettings {
|
||||
if (!raw) return { ...DEFAULT_GIT_SYNC_SETTINGS }
|
||||
try {
|
||||
const obj = JSON.parse(raw) as Partial<GitSyncSettings>
|
||||
const intervalMin = typeof obj.intervalMin === 'number' && obj.intervalMin >= 0 ? obj.intervalMin : 0
|
||||
return { intervalMin }
|
||||
} catch {
|
||||
return { ...DEFAULT_GIT_SYNC_SETTINGS }
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadGitSyncSettings(root: string): Promise<GitSyncSettings> {
|
||||
try {
|
||||
return parseGitSyncSettings(await dbPrefGet(`gitsync:${root}`))
|
||||
} catch {
|
||||
return { ...DEFAULT_GIT_SYNC_SETTINGS }
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveGitSyncSettings(root: string, settings: GitSyncSettings): Promise<void> {
|
||||
try {
|
||||
await dbPrefSet(`gitsync:${root}`, JSON.stringify(settings))
|
||||
} catch {
|
||||
// 偏好写入失败不挡同步本身
|
||||
}
|
||||
}
|
||||
|
||||
/** 侧栏同步条上的一句状态文案。 */
|
||||
export function gitStatusLabel(status: GitStatusInfo | null, syncing: boolean): string {
|
||||
if (syncing) return '正在同步…'
|
||||
if (!status) return '读取仓库状态…'
|
||||
if (!status.isRepo) return '未启用 Git 同步'
|
||||
const parts: string[] = []
|
||||
if (status.branch) parts.push(status.branch)
|
||||
if (!status.remoteUrl) parts.push('未设置远程')
|
||||
if (status.dirtyCount > 0) parts.push(`待提交 ${status.dirtyCount} 处`)
|
||||
if (status.ahead > 0) parts.push(`待推送 ${status.ahead}`)
|
||||
if (status.behind > 0) parts.push(`待拉取 ${status.behind}`)
|
||||
if (status.dirtyCount === 0 && status.ahead === 0 && status.behind === 0) parts.push('已同步')
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
/** 自动同步的提交信息(本地时间)。 */
|
||||
export function gitSyncCommitMessage(auto: boolean, now = new Date()): string {
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`
|
||||
return `${auto ? '自动同步' : '手动同步'} ${stamp}(new-markdown)`
|
||||
}
|
||||
|
||||
// ---- 定时同步租约:同一工作区只让一个窗口跑定时同步 ----
|
||||
|
||||
const LEASE_PREFIX = 'nm-gitsync-lease:'
|
||||
const LEASE_TTL_MS = 25_000
|
||||
|
||||
interface Lease {
|
||||
win: string
|
||||
ts: number
|
||||
}
|
||||
|
||||
/** 尝试取得/续租该工作区的定时同步租约;被别的窗口新鲜占用时返回 false。 */
|
||||
export function acquireGitSyncLease(root: string, windowId: string, now = Date.now()): boolean {
|
||||
try {
|
||||
const key = LEASE_PREFIX + root
|
||||
const raw = localStorage.getItem(key)
|
||||
if (raw) {
|
||||
const lease = JSON.parse(raw) as Lease
|
||||
if (lease.win !== windowId && now - lease.ts < LEASE_TTL_MS) return false
|
||||
}
|
||||
const mine: Lease = { win: windowId, ts: now }
|
||||
localStorage.setItem(key, JSON.stringify(mine))
|
||||
return true
|
||||
} catch {
|
||||
return true // 存储不可用时不拦(单窗口场景)
|
||||
}
|
||||
}
|
||||
|
||||
export function releaseGitSyncLease(root: string, windowId: string): void {
|
||||
try {
|
||||
const key = LEASE_PREFIX + root
|
||||
const raw = localStorage.getItem(key)
|
||||
if (raw && (JSON.parse(raw) as Lease).win === windowId) localStorage.removeItem(key)
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
@@ -311,3 +311,40 @@ export async function workspaceSearch(root: string, query: string, caseSensitive
|
||||
export async function revealInExplorer(path: string): Promise<void> {
|
||||
return invoke<void>('reveal_in_explorer', { path })
|
||||
}
|
||||
|
||||
export interface GitStatusInfo {
|
||||
isRepo: boolean
|
||||
branch: string | null
|
||||
remoteUrl: string | null
|
||||
dirtyCount: number
|
||||
ahead: number
|
||||
behind: number
|
||||
hasUpstream: boolean
|
||||
}
|
||||
|
||||
export interface GitSyncOutcome {
|
||||
committed: boolean
|
||||
pushed: boolean
|
||||
pulled: boolean
|
||||
summary: string
|
||||
}
|
||||
|
||||
/** 工作区的 Git 仓库状态(非仓库时 isRepo=false)。 */
|
||||
export async function gitStatus(root: string): Promise<GitStatusInfo> {
|
||||
return invoke<GitStatusInfo>('git_status', { root })
|
||||
}
|
||||
|
||||
/** 在工作区初始化 Git 仓库并绑定远程(远程可为空)。 */
|
||||
export async function gitInitRepo(root: string, remoteUrl: string, branch: string): Promise<GitStatusInfo> {
|
||||
return invoke<GitStatusInfo>('git_init_repo', { root, remoteUrl, branch })
|
||||
}
|
||||
|
||||
/** 设置/清除工作区仓库的 origin 远程(空字符串=移除)。 */
|
||||
export async function gitSetRemote(root: string, remoteUrl: string): Promise<GitStatusInfo> {
|
||||
return invoke<GitStatusInfo>('git_set_remote', { root, remoteUrl })
|
||||
}
|
||||
|
||||
/** 同步一次:提交本地更改→变基拉取远程→推送。冲突时返回错误且不留半成品。 */
|
||||
export async function gitSyncNow(root: string, message: string): Promise<GitSyncOutcome> {
|
||||
return invoke<GitSyncOutcome>('git_sync_now', { root, message })
|
||||
}
|
||||
|
||||
@@ -77,4 +77,7 @@ export function createWindowClaims(
|
||||
const randomWindowId = `win-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
||||
|
||||
/** 本应用窗口的认领单例(App 使用)。 */
|
||||
/** 本窗口的随机标识(草稿认领与 Git 定时同步租约共用)。 */
|
||||
export const appWindowId: string = randomWindowId
|
||||
|
||||
export const appWindowClaims: WindowClaims = createWindowClaims(randomWindowId)
|
||||
|
||||
@@ -416,3 +416,16 @@ kbd { background: var(--code-bg); border: 1px solid var(--border); border-radius
|
||||
*::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; border: 2px solid transparent; background-clip: content-box; }
|
||||
*::-webkit-scrollbar-thumb:hover { background: var(--text-dim); border: 2px solid transparent; background-clip: content-box; }
|
||||
*::-webkit-scrollbar-corner { background: transparent; }
|
||||
|
||||
/* Git 同步条(侧栏文件模式顶部) */
|
||||
.gitsync { border-bottom: 1px solid var(--border); padding: 6px 10px; }
|
||||
.gitsync-bar { display: flex; align-items: center; gap: 8px; min-height: 26px; }
|
||||
.gitsync-title { font-size: 12px; font-weight: 600; color: var(--text); white-space: nowrap; }
|
||||
.gitsync-status { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11.5px; color: var(--text-dim); }
|
||||
.gitsync-actions { display: flex; gap: 2px; }
|
||||
.gitsync-btn { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; border-radius: 6px; background: transparent; color: var(--text-dim); cursor: pointer; }
|
||||
.gitsync-btn:hover:not(:disabled) { background: var(--hover); color: var(--text); }
|
||||
.gitsync-btn:disabled { opacity: .5; cursor: default; }
|
||||
.gitsync-msg { margin-top: 4px; font-size: 11.5px; line-height: 1.45; color: var(--text-dim); overflow-wrap: anywhere; }
|
||||
.gitsync-msg.error { color: #cf5b52; }
|
||||
.modal-error { color: #cf5b52; font-size: 12px; line-height: 1.5; margin: 8px 0 0; }
|
||||
|
||||
Reference in New Issue
Block a user