diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2473e81..fed1dac 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -600,6 +600,57 @@ async fn p4_submit_change(state: State<'_, AppState>, change: String) -> Result< run_stream(&conn, &["submit", "-c", &change], "submit") } +/// Submit a shelved changelist directly from the server (`submit -e`), without +/// needing the files in the workspace. +#[tauri::command] +async fn p4_submit_shelved(state: State<'_, AppState>, change: String) -> Result { + let conn = current(&state)?; + run_stream(&conn, &["submit", "-e", change.trim()], "submit") +} + +/// Submit a numbered changelist with options: `reopen` keeps the files checked +/// out after submit (`-r`); `revert_unchanged` drops files with no real change +/// (`-f revertunchanged`). +#[tauri::command] +async fn p4_submit_opts(state: State<'_, AppState>, change: String, reopen: bool, revert_unchanged: bool) -> Result { + let conn = current(&state)?; + let ch = change.trim().to_string(); + let mut args: Vec = vec!["submit".into(), "-c".into(), ch]; + if reopen { + args.push("-r".into()); + } + if revert_unchanged { + args.push("-f".into()); + args.push("revertunchanged".into()); + } + let refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); + run_stream(&conn, &refs, "submit") +} + +/// Path of the workspace `.p4ignore` file (at the client root). +fn ignore_path(conn: &Conn) -> Option { + if conn.root.is_empty() { + return None; + } + Some(std::path::Path::new(&conn.root).join(".p4ignore")) +} + +/// Read the workspace `.p4ignore` (empty string if none yet). +#[tauri::command] +async fn p4_ignore_read(state: State<'_, AppState>) -> Result { + let conn = current(&state)?; + let p = ignore_path(&conn).ok_or("No workspace root known")?; + Ok(std::fs::read_to_string(&p).unwrap_or_default()) +} + +/// Write the workspace `.p4ignore`. +#[tauri::command] +async fn p4_ignore_write(state: State<'_, AppState>, content: String) -> Result<(), String> { + let conn = current(&state)?; + let p = ignore_path(&conn).ok_or("No workspace root known")?; + std::fs::write(&p, content).map_err(|e| format!("Could not write .p4ignore: {e}")) +} + /// "Uncommit": move files back into the default changelist (like `git reset`). /// The now-empty numbered changelist is cleaned up on the next `p4_changes`. #[tauri::command] @@ -2250,6 +2301,10 @@ pub fn run() { p4_filelog, p4_clean_preview, p4_clean_apply, + p4_submit_shelved, + p4_submit_opts, + p4_ignore_read, + p4_ignore_write, p4_jobs, p4_fix, p4_job_spec, diff --git a/src/App.tsx b/src/App.tsx index e0c3887..b0723d0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -315,6 +315,7 @@ type ModalState = | { kind: "streams" } | { kind: "jobs" } | { kind: "clean" } + | { kind: "ignore" } | { kind: "filelog"; depot: string; name: string } | { kind: "client"; name: string; isNew: boolean } | { kind: "blame"; spec: string; name: string } @@ -914,6 +915,19 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set try { await p4.shelve(change); flash(t("Shelved #{n} on the server.", { n: change })); } catch (e) { flash(String(e), true); } finally { setBusy(false); } } + // submit variants: reopen after submit (-r), revert-unchanged, or submit a shelf (-e) + async function submitWithOpts(change: string, reopen: boolean, revertUnchanged: boolean) { + if (!(await confirm({ title: t("Submit changelist #{n}?", { n: change }), body: t("This changelist goes to the Perforce server and becomes available to the team. This is a push — it cannot be undone."), confirm: t("Submit") }))) return; + setBusy(true); + try { await p4.submitOpts(change, reopen, revertUnchanged); flash(t("Submitted changelist #{n}.", { n: change })); clearDraftIfCommitted(); await refresh(); await loadHistory(); } + catch (e) { flash(String(e), true); setBusy(false); } + } + async function submitShelved(change: string) { + if (!(await confirm({ title: t("Submit shelved #{n}?", { n: change }), body: t("The shelved files of #{n} are submitted directly from the server.", { n: change }), confirm: t("Submit") }))) return; + setBusy(true); + try { await p4.submitShelved(change); flash(t("Submitted shelved changelist #{n}.", { n: change })); await refresh(); await loadHistory(); } + catch (e) { flash(String(e), true); setBusy(false); } + } async function unshelveCL(change: string) { setBusy(true); try { await p4.unshelve(change); flash(t("Unshelved #{n} into the workspace.", { n: change })); await refresh(); } @@ -1082,7 +1096,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set ...(uproject ? [{ label: (dockOpen && dockTab === "unreal" ? "✓ " : "") + t("Unreal Log"), icon: I.hex, act: () => openDock("unreal") }] : []), { label: t("File Locks…"), icon: I.lock, act: () => setModal({ kind: "locks" }) }, ], - Tools: [{ label: t("Search depot…"), icon: I.search, act: () => setModal({ kind: "search" }) }, { label: t("Exclusive Locks (typemap)…"), icon: I.lock, act: () => setModal({ kind: "typemap" }) }, { label: t("Labels…"), icon: I.clock, act: () => setModal({ kind: "labels" }) }, { label: t("Integrate / Merge / Copy…"), icon: I.branch, act: () => setModal({ kind: "branch" }) }, { label: t("Streams…"), icon: I.branch, act: () => setModal({ kind: "streams" }) }, { label: t("Jobs…"), icon: I.log, act: () => setModal({ kind: "jobs" }) }, { label: slnPath ? t("Build Solution (.sln)") : t("Build Solution — no .sln"), kb: "Ctrl+B", icon: I.hammer, act: startBuild }, { label: t("People & Roles…"), icon: I.people, act: () => setModal({ kind: "users" }) }, { label: t("Settings…"), icon: I.gear, act: () => setModal({ kind: "settings" }) }], + Tools: [{ label: t("Search depot…"), icon: I.search, act: () => setModal({ kind: "search" }) }, { label: t("Exclusive Locks (typemap)…"), icon: I.lock, act: () => setModal({ kind: "typemap" }) }, { label: t("Labels…"), icon: I.clock, act: () => setModal({ kind: "labels" }) }, { label: t("Integrate / Merge / Copy…"), icon: I.branch, act: () => setModal({ kind: "branch" }) }, { label: t("Streams…"), icon: I.branch, act: () => setModal({ kind: "streams" }) }, { label: t("Jobs…"), icon: I.log, act: () => setModal({ kind: "jobs" }) }, { label: t("Edit .p4ignore…"), icon: I.gear, act: () => setModal({ kind: "ignore" }) }, { label: slnPath ? t("Build Solution (.sln)") : t("Build Solution — no .sln"), kb: "Ctrl+B", icon: I.hammer, act: startBuild }, { label: t("People & Roles…"), icon: I.people, act: () => setModal({ kind: "users" }) }, { label: t("Settings…"), icon: I.gear, act: () => setModal({ kind: "settings" }) }], Help: [{ label: t("About"), icon: I.info, act: () => setModal({ kind: "about" }) }], }; @@ -1195,8 +1209,11 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set + +
{t("Files matching these patterns are ignored by reconcile / add. One pattern per line. Requires P4IGNORE to be configured (usually .p4ignore).")} + {!busy && !text.trim() && <> }
+ {busy ?
{t("Loading…")}
+ :