Skip to main content

Overview

The GitOps module provides stateful helpers for git operations within a pipeline run. All commands route through a &dyn Sandbox, enabling remote execution in Daytona mode while preserving local behavior. Key Features:
  • Branch creation with automatic collision detection (-2, -3, etc.)
  • Commit, push, PR creation via gh CLI
  • Changed file detection and diff generation
  • Branch-safe slug generation
  • Remote execution support via Sandbox abstraction

Core Type

GitOps<'a>

Stateful helper for git operations within a pipeline run.
pub struct GitOps<'a> {
    sandbox: &'a dyn Sandbox,
    base_branch: String,
    branch_name: Option<String>,
}

Constructor

new()

Creates a new GitOps instance.
pub fn new(sandbox: &'a dyn Sandbox, base_branch: String) -> Self
sandbox
&'a dyn Sandbox
required
Sandbox for executing git commands (local or remote)
base_branch
String
required
Base branch for PRs (e.g. “main”, “develop”)
Example:
use magpie_core::{GitOps, LocalSandbox};
use std::path::PathBuf;

let sandbox = LocalSandbox::from_path(PathBuf::from("/workspace/repo"));
let mut git = GitOps::new(&sandbox, "main".to_string());

Branch Operations

create_branch()

Creates and checks out a new branch magpie/{slug} from the base branch.
pub async fn create_branch(&mut self, task_description: &str) -> Result<String>
task_description
&str
required
Task description to slugify into a branch name
Result<String>
String
Returns the created branch name (e.g. “magpie/add-oauth-login”)
Behavior:
  1. Slugifies task description (lowercase, alphanumeric + hyphens, max 50 chars)
  2. Checks out base branch
  3. Pulls latest changes (if remote exists)
  4. Checks if magpie/{slug} already exists
  5. If it exists, appends -2, -3, etc. until finding an unused name
  6. Creates and checks out the new branch
  7. Stores branch name internally
Example:
let branch = git.create_branch("fix the login bug").await?;
assert_eq!(branch, "magpie/fix-the-login-bug");

// Second call with same task appends -2
let branch2 = git.create_branch("fix the login bug").await?;
assert_eq!(branch2, "magpie/fix-the-login-bug-2");

create_branch_from_slug()

Creates a branch from a pre-generated slug.
pub async fn create_branch_from_slug(&mut self, slug: &str) -> Result<String>
slug
&str
required
Pre-generated branch-safe slug (will be re-slugified for safety)
Result<String>
String
Returns the created branch name
Same behavior as create_branch() but uses the provided slug instead of slugifying a task description. Useful when branch names are generated by LLMs. Example:
let branch = git.create_branch_from_slug("add-oauth-login").await?;
assert_eq!(branch, "magpie/add-oauth-login");

branch_exists()

Checks whether a branch exists in the local repo.
pub async fn branch_exists(&self, branch: &str) -> bool
branch
&str
required
Branch name to check (e.g. “magpie/fix-bug”)
bool
bool
Returns true if the branch exists, false otherwise
Example:
let exists = git.branch_exists("magpie/add-oauth-login").await;
if exists {
    println!("Branch already exists!");
}

branch()

Returns the branch name created by create_branch().
pub fn branch(&self) -> Option<&str>
Option<&str>
&str
Returns the current branch name, or None if no branch has been created
Example:
git.create_branch("add feature").await?;
assert_eq!(git.branch(), Some("magpie/add-feature"));

checkout_base()

Switches back to the base branch.
pub async fn checkout_base(&self) -> Result<()>
Result<()>
()
Returns Ok(()) on success
Call this when the pipeline is done to leave the repo in a clean state. Example:
git.create_branch("experiment").await?;
// ... do work ...
git.checkout_base().await?; // Back to main/develop

Commit Operations

commit()

Stages all changes and commits.
pub async fn commit(&self, message: &str) -> Result<()>
message
&str
required
Commit message
Result<()>
()
Returns Ok(()) on success. Also returns Ok(()) if there’s nothing to commit.
Behavior:
  • Runs git add -A to stage all changes
  • Runs git commit -m "<message>"
  • “nothing to commit” is treated as success (not an error)
Example:
// Modify files...
git.commit("Add OAuth2 login endpoint").await?;

push()

Pushes the current branch to origin.
pub async fn push(&self) -> Result<()>
Result<()>
()
Returns Ok(()) on success
Behavior:
  • Runs git push -u origin <branch>
  • Uses the branch created by create_branch(), or falls back to base branch
Example:
git.create_branch("add-feature").await?;
// ... make changes ...
git.commit("Add feature").await?;
git.push().await?;

PR Operations

create_pr()

Creates a pull request via gh pr create.
pub async fn create_pr(&self, title: &str, body: &str, labels: &[&str]) -> Result<String>
title
&str
required
PR title
body
&str
required
PR description (supports GitHub-flavored markdown)
labels
&[&str]
required
PR labels (e.g. ["magpie", "automated"])
Result<String>
String
Returns the PR URL on success
Requires:
  • gh CLI installed and authenticated
  • Current branch pushed to origin
Example:
let pr_url = git.create_pr(
    "Add OAuth2 login",
    "Implements OAuth2 authentication flow.\n\n- Adds login endpoint\n- Adds token validation",
    &["magpie", "feature"],
).await?;

println!("PR created: {}", pr_url);

Diff & Change Detection

changed_files()

Gets the list of files changed on the current branch vs the base branch.
pub async fn changed_files(&self) -> Result<Vec<String>>
Result<Vec<String>>
Vec<String>
Returns list of changed file paths
Behavior:
  • Stages all changes with git add -A
  • Runs git diff --name-only <base_branch>
  • Returns file paths (one per line)
Example:
let files = git.changed_files().await?;
for file in files {
    println!("Changed: {}", file);
}

diff_stat()

Gets a short stat summary of changes.
pub async fn diff_stat(&self) -> Result<String>
Result<String>
String
Returns git diff --stat output
Example:
let stat = git.diff_stat().await?;
println!("Changes:\n{}", stat);
// Output:
// src/main.rs    | 10 ++++++++--
// tests/api.rs   |  5 +++++
// 2 files changed, 13 insertions(+), 2 deletions(-)

diff_content()

Gets the full diff of all changes.
pub async fn diff_content(&self) -> Result<String>
Result<String>
String
Returns git diff <base_branch> output
Example:
let diff = git.diff_content().await?;
println!("Full diff:\n{}", diff);

diff_name_status()

Gets changed file paths with their status (A/M/D).
pub async fn diff_name_status(&self) -> Result<Vec<(String, String)>>
Result<Vec<(String, String)>>
Vec<(status, path)>
Returns list of (status, path) tuples where status is “A” (added), “M” (modified), or “D” (deleted)
Example:
let changes = git.diff_name_status().await?;
for (status, path) in changes {
    println!("{} {}", status, path);
}
// Output:
// A src/auth.rs
// M src/main.rs
// D old_file.rs

Helper Functions

slugify()

Converts a task description into a branch-safe slug.
pub fn slugify(input: &str) -> String
input
&str
required
Text to slugify
String
String
Returns lowercase slug with hyphens, max 50 chars
Behavior:
  1. Convert to lowercase
  2. Replace non-alphanumeric with hyphens
  3. Collapse consecutive hyphens
  4. Trim leading/trailing hyphens
  5. Truncate to 50 chars (break at last hyphen before 50 if possible)
Example:
use magpie_core::git::slugify;

assert_eq!(slugify("Fix the login bug"), "fix-the-login-bug");
assert_eq!(slugify("Add API v2 (beta)!"), "add-api-v2-beta");
assert_eq!(slugify("fix   multiple   spaces"), "fix-multiple-spaces");
assert_eq!(slugify("---hello world---"), "hello-world");

Complete Example

use magpie_core::{GitOps, LocalSandbox};
use std::path::PathBuf;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialize GitOps
    let sandbox = LocalSandbox::from_path(PathBuf::from("/workspace/my-repo"));
    let mut git = GitOps::new(&sandbox, "main".to_string());

    // Create feature branch
    let branch = git.create_branch("add OAuth2 login").await?;
    println!("Created branch: {}", branch);

    // Make changes to files...
    // (use MagpieAgent or manual edits)

    // Check what changed
    let files = git.changed_files().await?;
    println!("Changed files: {:?}", files);

    let diff_stat = git.diff_stat().await?;
    println!("Diff stat:\n{}", diff_stat);

    // Commit changes
    git.commit("Add OAuth2 authentication flow").await?;

    // Push to remote
    git.push().await?;

    // Create PR
    let pr_url = git.create_pr(
        "Add OAuth2 login",
        "Implements OAuth2 authentication.\n\n- Login endpoint\n- Token validation\n- Refresh token support",
        &["magpie", "feature", "authentication"],
    ).await?;

    println!("PR created: {}", pr_url);

    // Clean up: switch back to main
    git.checkout_base().await?;

    Ok(())
}

Collision Handling

Branch creation automatically handles collisions by appending numeric suffixes:
// First run
let branch1 = git.create_branch("add feature").await?;
assert_eq!(branch1, "magpie/add-feature");

// Checkout base to simulate second run
git.checkout_base().await?;

// Second run with same task
let branch2 = git.create_branch("add feature").await?;
assert_eq!(branch2, "magpie/add-feature-2");

// Third run
git.checkout_base().await?;
let branch3 = git.create_branch("add feature").await?;
assert_eq!(branch3, "magpie/add-feature-3");
This prevents collisions when:
  • Pipeline is re-run for the same task
  • Multiple users work on similar tasks
  • Previous runs failed before merging

Sandbox Abstraction

All git commands route through &dyn Sandbox:
  • LocalSandbox: Executes commands via std::process::Command
  • DaytonaSandbox: Executes commands remotely via REST API
  • MockSandbox: Records commands for testing
This enables:
  • Remote execution in Daytona mode (commands run in cloud sandboxes)
  • Local execution for development and testing
  • Test isolation with MockSandbox
// Local execution
let local = LocalSandbox::from_path(PathBuf::from("/workspace/repo"));
let mut git_local = GitOps::new(&local, "main".to_string());

// Remote execution (requires Daytona config)
let daytona = DaytonaSandbox::create(&config, "org/repo").await?;
let mut git_remote = GitOps::new(&daytona, "main".to_string());

// Same API, different execution environment!

Error Handling

All git operations return Result<T> and can fail for several reasons:
  • Command not found: git or gh not installed
  • Authentication failure: No git credentials or gh not authenticated
  • Network failure: Can’t reach remote during push/PR creation
  • Conflict: Branch name collision limit exceeded (>100 suffixes)
  • No remote: Some operations require a remote (push, PR creation)
Errors include context for debugging:
match git.create_pr(title, body, labels).await {
    Ok(url) => println!("PR created: {}", url),
    Err(e) => eprintln!("PR creation failed: {:#}", e),
    // Example error: "gh pr create failed: not authenticated"
}

Build docs developers (and LLMs) love