aboutsummaryrefslogtreecommitdiff
path: root/mingling_ci/src/cmd/cmd_git_unlock.rs
blob: 4d9ee80c38911e4a362cf8d09395ee606e01dfe7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
use mingling::{
    Grouped, RenderResult, Routable,
    macros::{buffer, command, r_println, renderer},
    res::ResExitCode,
};

use crate::Next;
use crate::git::{LOCK_FILE, TEMP_COMMIT_MARK, head_message, run_git, worktree_clean};
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.
///
/// 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.
#[command(node = "git-unlock")]
pub fn git_unlock() -> 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();

    if let Err(e) = undo_ci_phase() {
        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"])?;
    run_git(["reset", "--hard", "HEAD~1"])?;
    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
}