Skip to main content

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}