scx_forge_agent/git.rs
1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: GPL-2.0-only
3//! In-place checkpoint/revert for the optimization loop, using the git index.
4//!
5//! The optimizer edits the scheduler crate in place on the current branch - it
6//! creates no branch and makes no commits. The git index is used as the
7//! checkpoint: an accepted round is staged (`git add`), and a rejected round is
8//! reverted by restoring the working tree from the index (`git checkout --`),
9//! which leaves earlier accepted changes intact and keeps cargo's incremental
10//! build cache warm. When the run ends the crate is unstaged, so the winning
11//! variant is left as ordinary (uncommitted) working-tree modifications.
12
13use std::path::Path;
14use std::process::Command;
15
16use anyhow::{bail, Context, Result};
17
18fn git(repo: &Path, args: &[&str]) -> Result<String> {
19 let out = Command::new("git")
20 .arg("-C")
21 .arg(repo)
22 .args(args)
23 .output()
24 .with_context(|| format!("spawn git {args:?}"))?;
25 if !out.status.success() {
26 bail!(
27 "git {args:?} failed: {}",
28 String::from_utf8_lossy(&out.stderr).trim()
29 );
30 }
31 Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
32}
33
34/// The crate directory must have no uncommitted changes before we start, so the
35/// index checkpoint begins at a known-clean baseline and a revert is safe.
36pub fn ensure_clean(repo: &Path, rel_crate: &str) -> Result<()> {
37 let status = git(repo, &["status", "--porcelain", "--", rel_crate])?;
38 if !status.is_empty() {
39 bail!(
40 "crate dir '{rel_crate}' has uncommitted changes; commit or stash them first:\n{status}"
41 );
42 }
43 Ok(())
44}
45
46pub fn rev_parse(repo: &Path, refname: &str) -> Result<String> {
47 git(repo, &["rev-parse", refname])
48}
49
50/// Revert the crate working tree to the last checkpoint by restoring it from the
51/// index (the last accepted state, or the baseline if nothing is accepted yet).
52pub fn discard(repo: &Path, rel_crate: &str) -> Result<()> {
53 git(repo, &["checkout", "--", rel_crate])?;
54 Ok(())
55}
56
57/// Checkpoint the current crate state as accepted by staging it into the index.
58pub fn checkpoint(repo: &Path, rel_crate: &str) -> Result<()> {
59 git(repo, &["add", "--", rel_crate])?;
60 Ok(())
61}
62
63/// Unstage the crate (reset the index to HEAD) while keeping working-tree edits,
64/// so an accepted result is left as plain uncommitted modifications.
65pub fn unstage(repo: &Path, rel_crate: &str) -> Result<()> {
66 git(repo, &["reset", "-q", "--", rel_crate])?;
67 Ok(())
68}
69
70/// Diff of the crate dir between `base_ref` and the working tree.
71pub fn diff(repo: &Path, base_ref: &str, rel_crate: &str) -> Result<String> {
72 git(repo, &["diff", base_ref, "--", rel_crate])
73}
74
75/// Diff of the crate dir between the index checkpoint and the working tree.
76pub fn worktree_diff(repo: &Path, rel_crate: &str) -> Result<String> {
77 git(repo, &["diff", "--", rel_crate])
78}