diff options
Diffstat (limited to 'mingling_ci/src')
| -rw-r--r-- | mingling_ci/src/cmd/cmd_git_lock.rs | 41 | ||||
| -rw-r--r-- | mingling_ci/src/cmd/cmd_git_unlock.rs | 39 | ||||
| -rw-r--r-- | mingling_ci/src/git.rs | 15 |
3 files changed, 67 insertions, 28 deletions
diff --git a/mingling_ci/src/cmd/cmd_git_lock.rs b/mingling_ci/src/cmd/cmd_git_lock.rs index 4398593..0e9bf22 100644 --- a/mingling_ci/src/cmd/cmd_git_lock.rs +++ b/mingling_ci/src/cmd/cmd_git_lock.rs @@ -5,46 +5,63 @@ use mingling::{ }; use crate::Next; -use crate::git::{LOCK_FILE, TEMP_COMMIT_MESSAGE, run_git, worktree_clean}; +use crate::git::{CI_TEMP_COMMIT_MESSAGE, LOCK_FILE, TEMP_COMMIT_MESSAGE, run_git, worktree_clean}; use crate::res::{CargoError, MessagePrinter}; /// Temporarily commits the workspace so CI can run on a stable tree. /// /// First pins the current HEAD to the `mingling/bkup` backup branch (created -/// or force-reset), then commits everything with a `[DO NOT PUSH] CI TEMP` -/// message. When the tree has no tracked changes, a `MINGLING-CI-CHECKING` -/// marker file is created first so the commit is never empty. +/// or force-reset). When the tree is dirty, all changes are packed into a +/// plain `TEMP` commit first so they can be restored later; the `CI TEMP` +/// commit then carries only the `MINGLING-CI-CHECKING` marker file, whose +/// content (`true`/`false`) tells `git-unlock` which restore path to take. #[command(node = "git-lock")] pub fn git_lock() -> Next { if let Err(e) = run_git(["branch", "-f", "mingling/bkup", "HEAD"]) { return ErrorGitLock(e).to_chain(); } - if worktree_clean() - && let Err(e) = std::fs::write(LOCK_FILE, "") - { + let dirty = !worktree_clean(); + if dirty { + if let Err(e) = run_git(["add", "."]) { + return ErrorGitLock(e).to_chain(); + } + if let Err(e) = run_git(["commit", "-m", TEMP_COMMIT_MESSAGE]) { + return ErrorGitLock(e).to_chain(); + } + } + + let marker = if dirty { "true" } else { "false" }; + if let Err(e) = std::fs::write(LOCK_FILE, marker) { return ErrorGitLock(format!("failed to create {LOCK_FILE}: {e}")).to_chain(); } if let Err(e) = run_git(["add", "."]) { return ErrorGitLock(e).to_chain(); } - if let Err(e) = run_git(["commit", "-m", TEMP_COMMIT_MESSAGE]) { + if let Err(e) = run_git(["commit", "-m", CI_TEMP_COMMIT_MESSAGE]) { return ErrorGitLock(e).to_chain(); } - ResultGitLock {}.to_chain() + ResultGitLock { dirty }.to_chain() } +/// Whether the tree was dirty (a base `TEMP` commit exists) when locking. #[derive(Grouped)] -pub struct ResultGitLock; +pub struct ResultGitLock { + dirty: bool, +} #[derive(Grouped, Default)] pub struct ErrorGitLock(pub String); #[renderer(buffer)] -pub fn render_git_lock(_: ResultGitLock) { - r_println!("Locked: CI temp commit created"); +pub fn render_git_lock(r: ResultGitLock) { + if r.dirty { + r_println!("Locked: dirty workspace committed for CI"); + } else { + r_println!("Locked: clean workspace marked for CI"); + } } #[renderer] diff --git a/mingling_ci/src/cmd/cmd_git_unlock.rs b/mingling_ci/src/cmd/cmd_git_unlock.rs index 4d9ee80..6d7389d 100644 --- a/mingling_ci/src/cmd/cmd_git_unlock.rs +++ b/mingling_ci/src/cmd/cmd_git_unlock.rs @@ -10,11 +10,14 @@ use crate::res::{CargoError, MessagePrinter}; /// Undoes a CI temporary commit created by [`crate::cmd::cmd_git_lock`]. /// -/// Only acts when the HEAD commit message contains `CI TEMP` (case-sensitive), -/// which together with the `MINGLING-CI-CHECKING` marker means the workspace -/// is in a CI phase and all uncommitted state may be discarded. Restores the -/// tree in five steps: unstage, restore tracked files, delete untracked files, -/// roll back the temporary commit, and remove the marker file. +/// 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 @@ -29,19 +32,33 @@ pub fn git_unlock() -> Next { // Record dirtiness before restoring: the restore discards those changes. let dirty = !worktree_clean(); - if let Err(e) = undo_ci_phase() { + // 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 let Err(e) = undo_ci_phase(based_on_dirty) { return ErrorGitUnlock(e).to_chain(); } ResultGitUnlock { dirty }.to_chain() } -/// The five-step restoration sequence of `git-unlock`. -fn undo_ci_phase() -> Result<(), String> { - run_git(["reset"])?; - run_git(["restore", "."])?; - run_git(["clean", "-f", "-d"])?; +/// 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(()) } diff --git a/mingling_ci/src/git.rs b/mingling_ci/src/git.rs index 39f8976..3d1404b 100644 --- a/mingling_ci/src/git.rs +++ b/mingling_ci/src/git.rs @@ -3,13 +3,18 @@ use std::ffi::OsStr; use std::process::Command; -/// Marker file created by `git-lock` when the working tree is clean, so that a -/// temporary commit can always be made; `git-unlock` removes it. Its presence -/// marks "CI phase in progress". +/// Marker file created by `git-lock` in the CI temporary commit; its content +/// is `true` when the tree was dirty (a base TEMP commit exists below) or +/// `false` when it was clean. `git-unlock` reads it to pick the restore path. pub(crate) const LOCK_FILE: &str = "MINGLING-CI-CHECKING"; -/// Temporary commit message used by `git-lock`. -pub(crate) const TEMP_COMMIT_MESSAGE: &str = "[DO NOT PUSH] CI TEMP [DO NOT PUSH]"; +/// First temporary commit: packs the dirty workspace changes so they can be +/// restored later. Only created when the tree is dirty. +pub(crate) const TEMP_COMMIT_MESSAGE: &str = "[DO NOT PUSH] TEMP [DO NOT PUSH]"; + +/// Second temporary commit: carries the marker file, and its message is what +/// `git-unlock` matches to confirm the CI phase. +pub(crate) const CI_TEMP_COMMIT_MESSAGE: &str = "[DO NOT PUSH] CI TEMP [DO NOT PUSH]"; /// Case-sensitive substring that identifies a CI temporary commit in the HEAD /// commit message. |
