aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mingling_ci/help.txt3
-rw-r--r--mingling_ci/src/cmd.rs2
-rw-r--r--mingling_ci/src/cmd/cmd_git_lock.rs60
-rw-r--r--mingling_ci/src/cmd/cmd_git_unlock.rs63
-rw-r--r--mingling_ci/src/git.rs57
-rw-r--r--mingling_ci/src/lib.rs1
6 files changed, 186 insertions, 0 deletions
diff --git a/mingling_ci/help.txt b/mingling_ci/help.txt
index 77d86d9..de006bb 100644
--- a/mingling_ci/help.txt
+++ b/mingling_ci/help.txt
@@ -11,6 +11,9 @@ COMMANDS:
report-collect Collect and organize all inspection reports
report-clean Clean up all reports
+ git-lock Temporarily commit the workspace for CI
+ git-unlock Restore the workspace after CI
+
show-features Print the docs.rs feature list of mingling
show-manifests Print all crate paths that need to be checked
diff --git a/mingling_ci/src/cmd.rs b/mingling_ci/src/cmd.rs
index 30d65a1..b9a02dc 100644
--- a/mingling_ci/src/cmd.rs
+++ b/mingling_ci/src/cmd.rs
@@ -1,3 +1,5 @@
+pub(crate) mod cmd_git_lock;
+pub(crate) mod cmd_git_unlock;
pub(crate) mod cmd_report_clean;
pub(crate) mod cmd_report_collect;
pub(crate) mod cmd_show_features;
diff --git a/mingling_ci/src/cmd/cmd_git_lock.rs b/mingling_ci/src/cmd/cmd_git_lock.rs
new file mode 100644
index 0000000..4398593
--- /dev/null
+++ b/mingling_ci/src/cmd/cmd_git_lock.rs
@@ -0,0 +1,60 @@
+use mingling::{
+ Grouped, RenderResult, Routable,
+ macros::{buffer, command, r_println, renderer},
+ res::ResExitCode,
+};
+
+use crate::Next;
+use crate::git::{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.
+#[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, "")
+ {
+ 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]) {
+ return ErrorGitLock(e).to_chain();
+ }
+
+ ResultGitLock {}.to_chain()
+}
+
+#[derive(Grouped)]
+pub struct ResultGitLock;
+
+#[derive(Grouped, Default)]
+pub struct ErrorGitLock(pub String);
+
+#[renderer(buffer)]
+pub fn render_git_lock(_: ResultGitLock) {
+ r_println!("Locked: CI temp commit created");
+}
+
+#[renderer]
+pub fn render_error_git_lock(
+ e: ErrorGitLock,
+ error: &CargoError,
+ exit_code: &mut ResExitCode,
+) -> RenderResult {
+ let render_result = RenderResult::new();
+ error.println(vec![format!("Git-Lock: {}", e.0)]);
+ exit_code.exit_code = 1;
+ render_result
+}
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..dadc318
--- /dev/null
+++ b/mingling_ci/src/cmd/cmd_git_unlock.rs
@@ -0,0 +1,63 @@
+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};
+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.
+#[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();
+ }
+
+ if let Err(e) = undo_ci_phase() {
+ return ErrorGitUnlock(e).to_chain();
+ }
+
+ ResultGitUnlock {}.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(())
+}
+
+#[derive(Grouped)]
+pub struct ResultGitUnlock;
+
+#[derive(Grouped, Default)]
+pub struct ErrorGitUnlock(pub String);
+
+#[renderer(buffer)]
+pub fn render_git_unlock(_: ResultGitUnlock) {
+ 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
+}
diff --git a/mingling_ci/src/git.rs b/mingling_ci/src/git.rs
new file mode 100644
index 0000000..39f8976
--- /dev/null
+++ b/mingling_ci/src/git.rs
@@ -0,0 +1,57 @@
+//! Thin wrappers around the `git` CLI used by the CI phase lock/unlock pair.
+
+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".
+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]";
+
+/// Case-sensitive substring that identifies a CI temporary commit in the HEAD
+/// commit message.
+pub(crate) const TEMP_COMMIT_MARK: &str = "CI TEMP";
+
+/// Runs `git <args>`, returning stdout on success.
+///
+/// # Errors
+///
+/// Returns the git error message (stderr) when the command exits non-zero, or
+/// when git itself cannot be spawned.
+pub(crate) fn run_git<I, S>(args: I) -> Result<String, String>
+where
+ I: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+{
+ let output = Command::new("git")
+ .args(args)
+ .output()
+ .map_err(|e| format!("failed to run git: {e}"))?;
+ if output.status.success() {
+ Ok(String::from_utf8_lossy(&output.stdout).into_owned())
+ } else {
+ Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
+ }
+}
+
+/// Returns `true` when `git diff-index --quiet HEAD --` succeeds, i.e. the
+/// working tree has no tracked changes. Git failures count as "not clean" so
+/// the caller falls back to the marker-file path.
+pub(crate) fn worktree_clean() -> bool {
+ Command::new("git")
+ .args(["diff-index", "--quiet", "HEAD", "--"])
+ .status()
+ .is_ok_and(|status| status.success())
+}
+
+/// The subject line of the HEAD commit.
+///
+/// # Errors
+///
+/// Returns the git error message when the log command fails.
+pub(crate) fn head_message() -> Result<String, String> {
+ run_git(["log", "-1", "--pretty=%s"]).map(|subject| subject.trim().to_string())
+}
diff --git a/mingling_ci/src/lib.rs b/mingling_ci/src/lib.rs
index 8ce8ce7..32a0cbd 100644
--- a/mingling_ci/src/lib.rs
+++ b/mingling_ci/src/lib.rs
@@ -6,6 +6,7 @@
use mingling::macros::{gen_program, help};
pub(crate) mod cmd;
+pub(crate) mod git;
pub(crate) mod task;
/// Mingling CI's Resources