Get Revision: sync workspace to a changelist / label / date
- Actions -> Get Revision... (enter CL number, label, or YYYY/MM/DD) - History right-click -> Sync workspace to #N - Backend p4 sync scope@rev with live transfer progress Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -485,6 +485,22 @@ async fn p4_sync(state: State<'_, AppState>, scope: String) -> Result<String, St
|
||||
Ok(if msg.is_empty() { "Already up to date — nothing to sync.".into() } else { msg })
|
||||
}
|
||||
|
||||
/// Sync the workspace to an exact revision: a changelist number, a label name,
|
||||
/// or a date (`YYYY/MM/DD`). `rev` is appended to the scope spec as `@rev`.
|
||||
#[tauri::command]
|
||||
async fn p4_sync_to(state: State<'_, AppState>, scope: String, rev: String) -> Result<String, String> {
|
||||
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<String, String> {
|
||||
@ -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,
|
||||
|
||||
15
src/App.tsx
15
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<string, { label: string; kb?: string; ext?: boolean; icon?: ReactNode; act?: () => 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
|
||||
<HistoryList history={history} busy={busy} sel={selChange} onSelect={openChange}
|
||||
onContext={(c, e) => 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")) },
|
||||
|
||||
@ -405,6 +405,13 @@ const D: Record<string, Tr> = {
|
||||
"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…" },
|
||||
};
|
||||
|
||||
@ -64,6 +64,7 @@ export const p4 = {
|
||||
history: (path: string) => invoke<Change[]>("p4_history", { path }),
|
||||
disconnect: () => invoke<void>("p4_disconnect"),
|
||||
sync: (scope = "") => invoke<string>("p4_sync", { scope }),
|
||||
syncTo: (scope: string, rev: string) => invoke<string>("p4_sync_to", { scope, rev }),
|
||||
diff: (file: string) => invoke<string>("p4_diff", { file }),
|
||||
revert: (files: string[]) => invoke<string>("p4_revert", { files }),
|
||||
submit: (description: string, files: string[]) =>
|
||||
|
||||
Reference in New Issue
Block a user