#[macro_use] extern crate clap;
extern crate git2;
mod args;
mod map;
mod filter;
use std::cmp;
use std::str;
use std::process;
use std::io::{self, Write};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use map::OidMap;
use filter::{Filter, filter_tree};
use args::Args;
fn is_empty_commit(commit: &git2::Commit, empty_tree: &git2::Oid) -> bool {
let mut parents = 0;
let mut same = 0;
for parent in commit.parents() {
if commit.tree_id() == parent.tree_id() {
same += 1;
}
parents += 1;
}
if parents > 0 {
parents == same
}
else {
commit.tree_id() == *empty_tree
}
}
fn process_commits(repo: &git2::Repository, revspec: &git2::Revspec,
map: &mut OidMap, filter: &Filter, quiet: bool)
-> Result<Option<git2::Oid>, git2::Error>
{
let mut commits = repo.revwalk()?;
commits.set_sorting(git2::SORT_TOPOLOGICAL | git2::SORT_REVERSE);
match (revspec.from(), revspec.to()) {
(Some(from), Some(to)) => {
commits.hide(from.id())?;
commits.push(to.id())?;
},
(Some(from), None) => {
commits.push(from.id())?;
},
_ => {
panic!("Invalid revspec");
},
};
let empty_tree = git2::Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?;
let mut last = None;
if !quiet {
println!("Getting list of commits...");
}
let commits = commits.collect::<Result<Vec<_>, git2::Error>>()?;
let status_step = cmp::max(commits.len() / 100, 1);
for (i, id) in commits.iter().enumerate() {
let id = id.clone();
if !quiet && i % status_step == 0 {
print!("\rRewriting {} ({}/{}) - {:3.0}%", id, i+1, commits.len(),
((i+1) as f32)/(commits.len() as f32) * 100.0);
io::stdout().flush().unwrap();
}
let commit = repo.find_commit(process_commit(repo, map, id, filter)?)?;
map.insert(id, Some(commit.id()));
if is_empty_commit(&commit, &empty_tree) {
if let Some(parent) = commit.parents().next() {
map.insert(commit.id(), Some(parent.id()));
}
else {
map.insert(commit.id(), None);
}
}
else {
last = Some(commit.id());
}
}
if let Some(commit) = last {
println!("\rRewriting {} ({}/{}) - 100%", commit, commits.len(),
commits.len());
}
Ok(last)
}
fn process_commit(repo: &git2::Repository, map: &mut OidMap, id: git2::Oid,
filter: &Filter)
-> Result<git2::Oid, git2::Error>
{
if let Some(&Some(newid)) = map.get(&id) {
return Ok(newid);
}
let commit = repo.find_commit(id)?;
let tree = commit.tree()?;
let newtree = filter_tree(repo, map, filter, &tree)?;
let parents : Vec<_> = commit
.parent_ids()
.filter_map(|p| {
match map.resolve(&p) {
Some(&Some(p)) => repo.find_commit(p).ok(),
_ => None,
}
}).collect();
let author = commit.author();
let committer = commit.committer();
repo.commit(
None,
&author,
&committer,
unsafe { str::from_utf8_unchecked(commit.message_bytes()) },
&repo.find_tree(newtree)?,
&parents.iter().collect::<Vec<_>>(), )
}
fn repo_subset(repo: &git2::Repository, map: &mut OidMap,
filter: &Filter, revspec: &str, branch: &str, force: bool, quiet: bool)
-> Result<bool, git2::Error>
{
let revspec = repo.revparse(revspec)?;
match process_commits(repo, &revspec, map, filter, quiet)? {
Some(oid) => {
let commit = repo.find_commit(oid)?;
repo.branch(branch, &commit, force)?;
Ok(true)
},
None => {
Ok(false)
},
}
}
fn main() {
let args = Args::parse();
let repo = match git2::Repository::open(args.repo) {
Ok(repo) => repo,
Err(err) => {
println!("Error: Failed to open repository: {}", err);
process::exit(1);
},
};
let mut filter = match args.filter_file {
Some(path) => match Filter::from_file(&path) {
Ok(filter) => filter,
Err(err) => {
println!("Error: Failed to load filter file '{}': {}", path, err);
process::exit(1);
},
},
None => Filter::new(),
};
for path in &args.paths {
filter.insert(path);
}
if filter.is_empty() {
println!("Error: Please specify paths to include with either \
`--filter-file` or `--path`.");
process::exit(1);
}
let map_name = {
let mut hasher = DefaultHasher::new();
filter.hash(&mut hasher);
format!("{:x}", hasher.finish())
};
let mut map = if args.nomap {
OidMap::new()
}
else {
match OidMap::from_repo(&repo, &map_name) {
Ok(map) => map,
Err(err) => {
println!("Error: Failed to load object map: {}", err);
process::exit(1);
},
}
};
match repo_subset(&repo, &mut map, &filter, &args.revspec, &args.branch,
args.force, args.quiet) {
Ok(true) => {
println!("Branch '{}' created.", args.branch);
},
Ok(false) => {
println!("Error: Filtering only produced empty commits. No branch \
created.");
process::exit(1);
},
Err(err) => {
println!("Error: Failed to create repository subset: {}", err);
process::exit(1);
},
};
if let Err(err) = map.write_repo(&repo, &map_name) {
println!("Error: Failed to write object map: {}", err);
process::exit(1);
}
}