Git Worktree Manager CLI Clone in Rust

A minimal 45‑minute clone of Worktrunk using only Rust stdlib

Welcome to another official UVF IT guide! In this quick build you’ll create a tiny CLI that mimics the core of Worktrunk – managing Git worktrees from the command line. We’ll stick to pure Rust and the system Git binary, so no extra services or paid APIs are required.

We’ll walk through scaffolding a Cargo project, adding a simple subcommand to list, add, and remove worktrees, run it locally, and verify it works with a real Git repo.

Before you start

Clone Architecture
LOCAL ENVIRONMENT exec reads/writes Rust CLI (src/main.rs) System Git binary Local Git repository
1

Phase 1: Scaffold the project

Create a new Cargo binary project that will hold our CLI. Cargo will generate the basic directory layout and a starter main.rs. Next we add clap – a lightweight argument parser – as a dependency so we can define subcommands later.

All of this is done from the terminal; no IDE configuration is required. Once the dependency is added, Cargo will fetch it the first time you build.

cargo new worktree-cli --bin
cd worktree-cli
cargo add clap --features derive
2

Phase 2: Core model – worktree struct & helper

We introduce a Worktree struct that mirrors the data we care about: path, branch, and whether it’s the current checkout. Then we write a small helper run_git that spawns the system git binary, captures its output, and returns a Result<String> – this keeps the rest of the code clean and testable.

The helper will be reused for all git commands (list, add, prune). By keeping everything in src/main.rs we stay minimal while still having a clear separation between data and execution logic.

src/main.rs

use std::process::{Command, Stdio};
use std::io::{self, Write};

#[derive(Debug)]
struct Worktree {
    path: String,
    branch: String,
    is_current: bool,
}

/// Executes a git command and returns stdout as a String.
/// On error we forward the command's stderr for easier debugging.
fn run_git(args: &[&str]) -> io::Result<String> {
    let output = Command::new("git")
        .args(args)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()?;
    if output.status.success() {
        Ok(String::from_utf8_lossy(&output.stdout).to_string())
    } else {
        let err = String::from_utf8_lossy(&output.stderr);
        Err(io::Error::new(io::ErrorKind::Other, err))
    }
}

fn main() {
    // Placeholder: argument parsing will be added in later phases.
    // For now we just verify the helper works against the current repo.
    match run_git(&["status", "--short"]) {
        Ok(status) => println!("Git status output:\n{}", status),
        Err(e) => eprintln!("Failed to run git: {}", e),
    }
}
3

Phase 3: API – list, add, prune commands

In this phase we wire up the command‑line interface using the clap crate. We expose three subcommands—list, add, and prune—that map directly to the helper functions you wrote earlier. Each subcommand parses its own arguments, calls the appropriate helper, and prints a friendly message for the user. Keeping the logic thin in main.rs makes the binary easy to extend later.

src/main.rs

use clap::{Parser, Subcommand};
mod helpers; // assumes list_worktrees, add_worktree, prune_worktrees are defined

#[derive(Parser)]
#[command(name = "worktrunk_clone")]
#[command(about = "Minimal Git worktree manager")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Show all worktrees for the repository
    List { path: String },
    /// Add a new worktree at <branch> under <dest>
    Add { repo: String, branch: String, dest: String },
    /// Remove stale worktree entries
    Prune { path: String },
}

fn main() {
    let cli = Cli::parse();
    match cli.command {
        Commands::List { path } => {
            helpers::list_worktrees(&path).expect("failed to list worktrees");
        }
        Commands::Add { repo, branch, dest } => {
            helpers::add_worktree(&repo, &branch, &dest).expect("failed to add worktree");
        }
        Commands::Prune { path } => {
            helpers::prune_worktrees(&path).expect("failed to prune worktrees");
        }
    }
}
4

Phase 4: Run locally

Now we compile the project and run a quick integration test against a temporary Git repository. The script creates a fresh repo, adds an initial commit, and then exercises the list and add commands to prove the clone works end‑to‑end. All steps are performed in /tmp/demo so they leave no trace on your machine after the test finishes.

#!/usr/bin/env bash
set -e
# 1. Build the binary
cargo build --release
# 2. Create a temporary repo
TMPDIR=$(mktemp -d)
cd $TMPDIR
git init .
echo "hello" > README.md
git add README.md
git commit -m "init"
# 3. Test the CLI
../target/release/worktrunk_clone list $TMPDIR
../target/release/worktrunk_clone add $TMPDIR master $TMPDIR/feature1
../target/release/worktrunk_clone list $TMPDIR
# 4. Clean up
cd /
rm -rf $TMPDIR
5

Phase 5: Verify & extend

After the script runs, you should see the original worktree listed, followed by the newly added feature1 directory. This confirms that the core commands behave as expected. As a next step you can add a --quiet flag to suppress informational output or implement a remove subcommand to delete a worktree cleanly.

#!/usr/bin/env bash
set -e
# Verify that list prints at least one line
output=$(./target/release/worktrunk_clone list /tmp/demo)
lines=$(echo "$output" | wc -l)
if [ $lines -lt 1 ]; then
  echo "FAIL: list produced no output"
  exit 1
fi
# Verify that the added worktree directory exists
if [ ! -d /tmp/demo/feature1 ]; then
  echo "FAIL: added worktree not found"
  exit 1
fi
echo "All checks passed!"

Verifying the Stack

CheckCommand / Why it matters
Cargo builds without errorscargo build --release returns exit code 0
list prints at least one line (the main worktree)./target/release/worktrunk_clone list | wc -l >= 1
add creates a new worktree directoryls /tmp/demo/feature1 && echo "exists"

What’s next?

You now have a functional subset of Worktrunk. Feel free to add more subcommands (remove, checkout), improve error handling, or package the binary for distribution.