diff options
Diffstat (limited to 'mingling_ci/src/cmd/cmd_git_unlock.rs')
| -rw-r--r-- | mingling_ci/src/cmd/cmd_git_unlock.rs | 116 |
1 files changed, 116 insertions, 0 deletions
diff --git a/mingling_ci/src/cmd/cmd_git_unlock.rs b/mingling_ci/src/cmd/cmd_git_unlock.rs new file mode 100644 index 0000000..41efefc --- /dev/null +++ b/mingling_ci/src/cmd/cmd_git_unlock.rs @@ -0,0 +1,116 @@ +use mingling::{ + Grouped, RenderResult, Routable, + macros::{arg, buffer, command, r_println, renderer}, + picker::{EntryPicker, value::Flag}, + res::ResExitCode, +}; + +use crate::git::{LOCK_FILE, TEMP_COMMIT_MARK, head_message, run_git, worktree_clean}; +use crate::res::{CargoError, MessagePrinter}; +use crate::{Entry, Next}; + +/// Undoes a CI temporary commit created by [`crate::cmd::cmd_git_lock`]. +/// +/// Only acts when the HEAD commit message contains `CI TEMP` (case-sensitive). +/// The restore path is picked by the marker file content: +/// +/// - `true`: a base `TEMP` commit with the dirty changes sits below; restore +/// by hard-resetting past the marker commit, then soft-resetting and +/// unstaging to put the user's changes back into the working tree. +/// - `false`: the tree was clean; a single hard reset back to the original +/// HEAD is enough. +/// +/// When the working tree is dirty (e.g. CI left tracked changes behind) the +/// restore still runs, but the command reports a non-zero exit code so the +/// caller knows the CI phase contaminated the repository. With `--show-diff` +/// the diff of those changes is printed before they are discarded. +#[command(node = "git-unlock")] +// `#[command]` rewrites an owned first param into the entry type, so the args +// must be passed by value even though the body only reads them. +#[allow(clippy::needless_pass_by_value)] +pub fn git_unlock(args: Entry) -> Next { + let head = head_message().unwrap_or_default(); + if !head.contains(TEMP_COMMIT_MARK) { + return ErrorGitUnlock(format!("HEAD is not a CI temporary commit: `{head}`")).to_chain(); + } + + // Record dirtiness before restoring: the restore discards those changes. + let dirty = !worktree_clean(); + + // The marker file lives in the HEAD (CI TEMP) commit, so it is readable + // from the working tree; a missing marker falls back to the clean path. + let based_on_dirty = + std::fs::read_to_string(LOCK_FILE).is_ok_and(|content| content.trim() == "true"); + + if dirty && *args.pick(&arg![show_diff: Flag]).unwrap() { + show_diff(); + } + + if let Err(e) = undo_ci_phase(based_on_dirty) { + return ErrorGitUnlock(e).to_chain(); + } + + ResultGitUnlock { dirty }.to_chain() +} + +/// Prints the tracked changes the CI run left behind, before the restore +/// discards them. Untracked files are not shown (they are removed by clean). +fn show_diff() { + let Ok(diff) = run_git(["diff", "HEAD"]) else { + return; + }; + if diff.is_empty() { + return; + } + println!("{diff}"); +} + +/// Restores the workspace, keeping the user's pre-lock changes. +/// +/// With a base `TEMP` commit (`true`) the marker commit is dropped by a hard +/// reset to `HEAD~1`, the `TEMP` commit is unwrapped into the staging area by +/// a soft reset, and a plain reset unstages it back into the working tree. +/// Without one (`false`) a single hard reset to `HEAD~1` removes the marker +/// commit and lands on the original HEAD. +fn undo_ci_phase(based_on_dirty: bool) -> Result<(), String> { + run_git(["reset", "--hard", "HEAD~1"])?; + if based_on_dirty { + // Unwrap the `TEMP` commit into the staging area, then unstage it + // back into the working tree. + run_git(["reset", "--soft", "HEAD~1"])?; + run_git(["reset"])?; + } + std::fs::remove_file(LOCK_FILE).ok(); + Ok(()) +} + +/// Whether the working tree was dirty when the unlock started. +#[derive(Grouped)] +pub struct ResultGitUnlock { + dirty: bool, +} + +#[derive(Grouped, Default)] +pub struct ErrorGitUnlock(pub String); + +#[renderer(buffer)] +pub fn render_git_unlock(r: ResultGitUnlock, exit_code: &mut ResExitCode) { + if r.dirty { + r_println!("Unlocked: workspace restored (working tree was dirty)"); + exit_code.exit_code = 1; + } else { + r_println!("Unlocked: workspace restored"); + } +} + +#[renderer] +pub fn render_error_git_unlock( + e: ErrorGitUnlock, + error: &CargoError, + exit_code: &mut ResExitCode, +) -> RenderResult { + let render_result = RenderResult::new(); + error.println(vec![format!("Git-Unlock: {}", e.0)]); + exit_code.exit_code = 1; + render_result +} |
