diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4114cbd..21d6eab 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -485,6 +485,22 @@ async fn p4_sync(state: State<'_, AppState>, scope: String) -> Result, scope: String, rev: String) -> Result { + let conn = current(&state)?; + let rev = rev.trim(); + if rev.is_empty() { + return Err("No revision specified".into()); + } + // strip a leading @ or # if the user typed one + let rev = rev.trim_start_matches(['@', '#']); + let spec = format!("{}@{}", scope_spec(&scope), rev); + let msg = run_stream(&conn, &["sync", &spec], "sync")?; + Ok(if msg.is_empty() { "Already at that revision — nothing to sync.".into() } else { msg }) +} + /// Unified diff of an opened file vs. the have revision. #[tauri::command] async fn p4_diff(state: State<'_, AppState>, file: String) -> Result { @@ -2008,6 +2024,7 @@ pub fn run() { p4_typemap_get, p4_typemap_set, p4_reopen_type, + p4_sync_to, uasset_thumbnail, uasset_export_3d, p4_shelve, diff --git a/src/App.tsx b/src/App.tsx index 09d19c6..bac81d0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -758,6 +758,18 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set try { flash(t("Get Latest: {msg}", { msg: await p4.sync(activePath) })); await refresh(); await loadHistory(); } // sync may pull new submitted changelists → refresh History too catch (e) { flash(String(e), true); setBusy(false); } } + // sync the workspace to an exact changelist / label / date (Get Revision) + async function syncTo(rev: string) { + const r = rev.trim(); + if (!r) return; + if (!(await confirm({ title: t("Sync workspace to {rev}?", { rev: r }), body: t("Your synced files change to match {rev}. Un-submitted (opened) work is not touched. You can Get Latest to return to head.", { rev: r }), confirm: t("Sync") }))) return; + setBusy(true); + try { flash(t("Synced to {rev}: {msg}", { rev: r, msg: await p4.syncTo(activePath, r) })); await refresh(); await loadHistory(); } + catch (e) { flash(String(e), true); setBusy(false); } + } + function promptSyncTo() { + setPrompt({ title: t("Get Revision"), label: t("Changelist number, label name, or date (YYYY/MM/DD)"), value: "", onSave: (v) => { setPrompt(null); syncTo(v); } }); + } // Manual re-scan (same background reconcile as auto, on demand). function rescan() { refresh(activePath, true); } @@ -1035,7 +1047,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set const menus: Record void }[]> = { File: [{ label: t("Switch workspace…"), icon: I.monitor, act: openWorkspaces }, { label: t("Choose working folder…"), icon: I.folder, act: () => browseTo("") }, { label: t("Disconnect"), icon: I.power, act: onDisconnect }], Connection: [{ label: t("Refresh"), kb: "F5", icon: I.sync, act: () => refresh() }, { label: t("Choose working folder…"), icon: I.folder, act: () => browseTo("") }], - Actions: [{ label: t("Rescan changes"), kb: "Ctrl+R", icon: I.scan, act: rescan }, { label: t("Get Latest"), kb: "Ctrl+G", icon: I.sync, act: getLatest }, { label: t("Commit (local)"), icon: I.check, act: doCommit }, { label: t("Submit to server"), kb: "Ctrl+S", icon: I.up, act: pushAll }, ...(needResolve.length ? [{ label: t("Resolve conflicts ({n})", { n: needResolve.length }), icon: I.hex, act: () => setResolveOpen(true) }] : []), { label: t("Revert All…"), icon: I.revert, act: revertAll }, { label: t("Refresh"), kb: "F5", icon: I.sync, act: () => refresh() }], + Actions: [{ label: t("Rescan changes"), kb: "Ctrl+R", icon: I.scan, act: rescan }, { label: t("Get Latest"), kb: "Ctrl+G", icon: I.sync, act: getLatest }, { label: t("Get Revision…"), icon: I.clock, act: promptSyncTo }, { label: t("Commit (local)"), icon: I.check, act: doCommit }, { label: t("Submit to server"), kb: "Ctrl+S", icon: I.up, act: pushAll }, ...(needResolve.length ? [{ label: t("Resolve conflicts ({n})", { n: needResolve.length }), icon: I.hex, act: () => setResolveOpen(true) }] : []), { label: t("Revert All…"), icon: I.revert, act: revertAll }, { label: t("Refresh"), kb: "F5", icon: I.sync, act: () => refresh() }], Window: [ { label: (dockOpen && dockTab === "log" ? "✓ " : "") + t("Log"), kb: "Ctrl+L", icon: I.log, act: () => openDock("log") }, { label: (dockOpen && dockTab === "terminal" ? "✓ " : "") + t("Terminal"), kb: "Ctrl+`", icon: I.terminal, act: () => openDock("terminal") }, @@ -1226,6 +1238,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set openCtx(e, [ { label: t("Open this changelist"), act: () => openChange(c) }, + { label: t("Sync workspace to #{n}", { n: c.change || "" }), icon: I.clock, act: () => syncTo(c.change || "") }, { label: t("Revert (undo) #{n}", { n: c.change || "" }), danger: true, act: () => revertChange(c) }, { label: t("Copy number #{n}", { n: c.change || "" }), act: () => copyText(c.change || "", t("Number")) }, { label: t("Copy description"), act: () => copyText((c.desc || "").trim(), t("Description")) }, diff --git a/src/i18n.ts b/src/i18n.ts index ee3c176..03c004c 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -405,6 +405,13 @@ const D: Record = { "Typemap saved.": { ru: "Typemap сохранён.", de: "Typemap gespeichert.", fr: "Typemap enregistrée.", es: "Typemap guardada." }, "Save typemap": { ru: "Сохранить typemap", de: "Typemap speichern", fr: "Enregistrer la typemap", es: "Guardar typemap" }, "Remove": { ru: "Убрать", de: "Entfernen", fr: "Retirer", es: "Quitar" }, + "Get Revision…": { ru: "Взять ревизию…", de: "Revision holen…", fr: "Obtenir une révision…", es: "Obtener revisión…" }, + "Get Revision": { ru: "Взять ревизию", de: "Revision holen", fr: "Obtenir une révision", es: "Obtener revisión" }, + "Changelist number, label name, or date (YYYY/MM/DD)": { ru: "Номер changelist, имя метки или дата (ГГГГ/ММ/ДД)", de: "Changelist-Nummer, Label-Name oder Datum (JJJJ/MM/TT)", fr: "Numéro de changelist, nom de label ou date (AAAA/MM/JJ)", es: "Número de changelist, nombre de etiqueta o fecha (AAAA/MM/DD)" }, + "Sync workspace to {rev}?": { ru: "Синкнуть воркспейс на {rev}?", de: "Arbeitsbereich auf {rev} synchronisieren?", fr: "Synchroniser l'espace de travail sur {rev} ?", es: "¿Sincronizar el espacio de trabajo a {rev}?" }, + "Your synced files change to match {rev}. Un-submitted (opened) work is not touched. You can Get Latest to return to head.": { ru: "Синкнутые файлы изменятся под {rev}. Незасабмиченные (открытые) правки не тронутся. Get Latest вернёт на голову.", de: "Deine synchronisierten Dateien werden an {rev} angepasst. Nicht übermittelte (offene) Arbeit bleibt unberührt. Mit „Get Latest“ kehrst du zum Head zurück.", fr: "Vos fichiers synchronisés passent à {rev}. Le travail non soumis (ouvert) n'est pas touché. « Get Latest » vous ramène à la tête.", es: "Tus archivos sincronizados cambian a {rev}. El trabajo no enviado (abierto) no se toca. Con «Get Latest» vuelves a la cabeza." }, + "Synced to {rev}: {msg}": { ru: "Синкнуто на {rev}: {msg}", de: "Auf {rev} synchronisiert: {msg}", fr: "Synchronisé sur {rev} : {msg}", es: "Sincronizado a {rev}: {msg}" }, + "Sync workspace to #{n}": { ru: "Синкнуть воркспейс на #{n}", de: "Arbeitsbereich auf #{n} synchronisieren", fr: "Synchroniser l'espace de travail sur #{n}", es: "Sincronizar el espacio de trabajo a #{n}" }, "Pick the Unreal project working folder first.": { ru: "Сначала выбери рабочую папку Unreal-проекта.", de: "Wähle zuerst den Arbeitsordner des Unreal-Projekts.", fr: "Choisis d'abord le dossier de travail du projet Unreal.", es: "Primero elige la carpeta de trabajo del proyecto Unreal." }, "Disconnected from the server — retrying…": { ru: "Соединение с сервером потеряно — переподключаюсь…", de: "Verbindung zum Server verloren — erneuter Versuch…", fr: "Déconnecté du serveur — nouvelle tentative…", es: "Desconectado del servidor — reintentando…" }, }; diff --git a/src/p4.ts b/src/p4.ts index a59aadd..c11fbdd 100644 --- a/src/p4.ts +++ b/src/p4.ts @@ -64,6 +64,7 @@ export const p4 = { history: (path: string) => invoke("p4_history", { path }), disconnect: () => invoke("p4_disconnect"), sync: (scope = "") => invoke("p4_sync", { scope }), + syncTo: (scope: string, rev: string) => invoke("p4_sync_to", { scope, rev }), diff: (file: string) => invoke("p4_diff", { file }), revert: (files: string[]) => invoke("p4_revert", { files }), submit: (description: string, files: string[]) =>