Multi-Repository Refactoring
MultiRepoRefactor enables applying the same refactoring operation across multiple repositories at once.
Basic Usage
#![allow(unused)]
fn main() {
use refactor::prelude::*;
MultiRepoRefactor::new()
.repo("./project-a")
.repo("./project-b")
.repo("./project-c")
.matching(|m| m
.git(|g| g.branch("main").clean())
.files(|f| f.extension("rs")))
.transform(|t| t
.replace_literal("old_api", "new_api"))
.apply()?;
}
Adding Repositories
Individual Repos
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.repo("./project-a")
.repo("./project-b")
}
Multiple at Once
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.repos(["./project-a", "./project-b", "./project-c"])
}
Discover in Directory
Find all Git repositories in a parent directory:
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.discover("./workspace")? // Finds all dirs with .git
}
Filtering Repositories
Use Git matchers to filter which repositories to process:
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
.git(|g| g
.has_file("Cargo.toml") // Only Rust projects
.branch("main") // On main branch
.recent_commits(30) // Active in last 30 days
.clean())) // No uncommitted changes
.transform(/* ... */)
.apply()?;
}
Applying Transforms
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
.git(|g| g.has_file("Cargo.toml"))
.files(|f| f.extension("rs")))
.transform(|t| t
.replace_pattern(r"\.unwrap\(\)", ".expect(\"error\")"))
.dry_run() // Preview first!
.apply()?;
}
Handling Results
The result is a vector of per-repository results:
#![allow(unused)]
fn main() {
let results = MultiRepoRefactor::new()
.repos(["./project-a", "./project-b"])
.transform(|t| t.replace_literal("old", "new"))
.apply()?;
for (repo_path, result) in results {
match result {
Ok(ref_result) => {
println!("{}: modified {} files",
repo_path.display(),
ref_result.files_modified());
}
Err(e) => {
println!("{}: error - {}",
repo_path.display(),
e);
}
}
}
}
Dry Run Mode
Always preview changes across all repos first:
#![allow(unused)]
fn main() {
let results = MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m.files(|f| f.extension("rs")))
.transform(|t| t.replace_literal("old_name", "new_name"))
.dry_run()
.apply()?;
for (path, result) in &results {
if let Ok(r) = result {
if r.files_modified() > 0 {
println!("\n=== {} ===", path.display());
println!("{}", r.diff());
}
}
}
}
Complete Example
Update a deprecated API across all Rust projects in a workspace:
#![allow(unused)]
fn main() {
use refactor::prelude::*;
fn update_api_across_workspace() -> Result<()> {
let results = MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
// Only Rust projects on main branch
.git(|g| g
.has_file("Cargo.toml")
.branch("main")
.clean())
// Only .rs files with the old API
.files(|f| f
.extension("rs")
.exclude("**/target/**")
.contains_pattern("deprecated_function")))
.transform(|t| t
.replace_pattern(
r"deprecated_function\((.*?)\)",
"new_function($1, Default::default())"
))
.dry_run()
.apply()?;
// Summary
let mut total_files = 0;
let mut repos_modified = 0;
for (path, result) in &results {
if let Ok(r) = result {
let files = r.files_modified();
if files > 0 {
repos_modified += 1;
total_files += files;
println!("{}: {} files", path.display(), files);
}
}
}
println!("\nTotal: {} files across {} repositories",
total_files, repos_modified);
Ok(())
}
}
Error Handling
Individual repository failures don’t stop the whole operation:
#![allow(unused)]
fn main() {
let results = multi_refactor.apply()?;
let (successes, failures): (Vec<_>, Vec<_>) = results
.into_iter()
.partition(|(_, r)| r.is_ok());
println!("Succeeded: {} repos", successes.len());
println!("Failed: {} repos", failures.len());
for (path, err) in failures {
println!(" {}: {}", path.display(), err.unwrap_err());
}
}
Use Cases
Dependency Updates
#![allow(unused)]
fn main() {
// Update version in all Cargo.toml files
MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
.files(|f| f.name_matches(r"^Cargo\.toml$")))
.transform(|t| t
.replace_pattern(
r#"my-lib = "1\.0""#,
r#"my-lib = "2.0""#
))
.apply()?;
}
Code Style Enforcement
#![allow(unused)]
fn main() {
// Add missing newlines at end of files
MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
.git(|g| g.has_file("Cargo.toml"))
.files(|f| f.extension("rs")))
.transform(|t| t
.replace_pattern(r"([^\n])$", "$1\n"))
.apply()?;
}
License Header Updates
#![allow(unused)]
fn main() {
MultiRepoRefactor::new()
.discover("./workspace")?
.matching(|m| m
.files(|f| f
.extension("rs")
.contains_pattern("// Copyright 2023")))
.transform(|t| t
.replace_literal("// Copyright 2023", "// Copyright 2024"))
.apply()?;
}
Limitations
- Transforms are applied independently to each repo
- No cross-repository dependency resolution
- Large workspaces may be slow (consider parallel processing)
Tips
- Always use dry_run first - Review changes before applying
- Use specific matchers - Avoid modifying unexpected repos
- Require clean state - Use
.git(|g| g.clean())to avoid conflicts - Check branch - Ensure you’re on the right branch
- Commit separately - Each repo should be committed individually