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 for executing git commands (local or remote)
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 to slugify into a branch name
Returns the created branch name (e.g. “magpie/add-oauth-login”)
Behavior:
- Slugifies task description (lowercase, alphanumeric + hyphens, max 50 chars)
- Checks out base branch
- Pulls latest changes (if remote exists)
- Checks if
magpie/{slug} already exists
- If it exists, appends
-2, -3, etc. until finding an unused name
- Creates and checks out the new branch
- 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>
Pre-generated branch-safe slug (will be re-slugified for safety)
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 name to check (e.g. “magpie/fix-bug”)
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>
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<()>
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<()>
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<()>
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>
PR description (supports GitHub-flavored markdown)
PR labels (e.g. ["magpie", "automated"])
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>>
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>
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>
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)>>
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
Returns lowercase slug with hyphens, max 50 chars
Behavior:
- Convert to lowercase
- Replace non-alphanumeric with hyphens
- Collapse consecutive hyphens
- Trim leading/trailing hyphens
- 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"
}