use chrono;
use env_logger;
use git2;
use libpijul;
use rand;
#[macro_use]
extern crate clap;
#[macro_use]
extern crate log;
use chrono::TimeZone;
use clap::{App, Arg};
use git2::*;
use libpijul::fs_representation;
use libpijul::fs_representation::{RepoPath, RepoRoot};
use libpijul::patch::PatchFlags;
use rand::Rng;
use std::collections::hash_map::Entry::{Occupied, Vacant};
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::BufWriter;
use std::io::Write;
use std::path::{Path, PathBuf};
fn main() {
env_logger::init();
let matches = App::new("Git -> Pijul converter")
.version(crate_version!())
.about("Converts a Git repository into a Pijul one")
.arg(
Arg::with_name("INPUT")
.help("Sets the input Git repository.")
.required(true)
.index(1),
)
.get_matches();
let repo_root = Path::new(matches.value_of("INPUT").unwrap());
let repo = Repository::open(repo_root).unwrap();
let commits = get_commits(&repo);
let pijul_dir = repo_root.join(".pijul");
let commit_log_name = pijul_dir.join("git-converted-commits");
match File::open(&commit_log_name) {
Ok(prev_commit_log) => {
let reader = BufReader::new(prev_commit_log);
for line in reader.lines() {
println!("already processed {}", line.unwrap());
}
}
Err(_) => {
debug!("create new repo");
std::fs::remove_dir_all(&pijul_dir).unwrap_or(());
fs_representation::create(&repo_root, rand::thread_rng()).unwrap();
File::create(&commit_log_name).unwrap();
}
};
let commit_log = OpenOptions::new()
.append(true)
.open(&commit_log_name)
.unwrap();
let mut writer = BufWriter::new(commit_log);
let repo_root = if let Some(r) = fs_representation::find_repo_root(&repo_root) {
r
} else {
error!("no repo");
return;
};
let pristine_dir = repo_root.pristine_dir();
let mut current_branch_name = "master";
for &(commit_id, ref branch_name, ref forks) in commits.iter() {
let mut increase = 409600;
if current_branch_name != branch_name {
let _res = loop {
match switch_branch(current_branch_name, branch_name, &repo_root, increase) {
Err(ref e) if e.lacks_space() => increase *= 2,
e => break e,
}
};
current_branch_name = branch_name;
}
debug!("id {:?}", commit_id);
let commit = repo.find_commit(commit_id).unwrap();
let mut checkout = build::CheckoutBuilder::new();
checkout.force();
repo.checkout_tree(commit.as_object(), Some(&mut checkout))
.unwrap();
let files = loop {
match file_moves(&repo, &repo_root, &commit, &pristine_dir, increase) {
Err(ref e) if e.lacks_space() => increase *= 2,
e => break e,
}
};
let author = commit.author();
record(
&repo_root,
&branch_name,
libpijul::PatchHeader {
authors: vec![format!(
"{} <{}>",
author.name().unwrap().to_string(),
author.email().unwrap().to_string()
)],
name: commit.message().unwrap().to_string(),
description: None,
timestamp: chrono::Utc.timestamp(author.when().seconds(), 0),
flag: PatchFlags::empty(),
},
files.unwrap(),
);
let new_repo = libpijul::Repository::open(&pristine_dir, None).unwrap();
for fork in forks {
debug!("creating branch {:?}", fork);
let mut txn = new_repo.mut_txn_begin(rand::thread_rng()).unwrap();
let branch = txn.open_branch(&branch_name).unwrap();
let new_branch = txn.fork(&branch, &fork).unwrap();
let _res1 = txn.commit_branch(branch);
let _res2 = txn.commit_branch(new_branch);
let _res3 = txn.commit();
}
writeln!(writer, "{}", commit_id);
writer.flush().unwrap();
}
}
fn get_commits(
repo: &git2::Repository,
) -> Vec<(
git2::Oid,
std::string::String,
std::vec::Vec<std::string::String>,
)> {
let mut walk = repo.revwalk().unwrap();
walk.set_sorting(git2::Sort::TOPOLOGICAL);
let mut commit_to_branch = HashMap::new();
let branches = match repo.branches(Some(git2::BranchType::Local)) {
Err(e) => {
eprint!("{}", e.message());
panic!(e)
}
Ok(r) => r,
};
for branch in branches {
let branch = branch.unwrap().0;
let commit = branch.get().target().unwrap();
let _res = walk.push(commit);
commit_to_branch.insert(
commit,
(branch.name().unwrap().unwrap().to_owned(), Vec::new()),
);
}
let mut commits_reverse = Vec::new();
for commit in walk {
let commit = commit.unwrap();
let (current_branch, forks) = commit_to_branch.get(&commit).unwrap().clone();
for parent in repo.find_commit(commit).unwrap().parents() {
match commit_to_branch.entry(parent.id()) {
Vacant(entry) => {
entry.insert((current_branch.clone(), Vec::new()));
}
Occupied(mut entry) => {
let ref mut parent_forks = entry.get_mut().1;
parent_forks.push(current_branch.clone());
}
};
}
debug!(
"commit {:?} in branch {:?} with forks {:?}",
commit, current_branch, forks
);
commits_reverse.push((commit, current_branch, forks));
}
commits_reverse.reverse();
commits_reverse
}
fn switch_branch(
current_branch_name: &str,
branch_name: &str,
repo_root: &RepoRoot<PathBuf>,
increase: u64,
) -> libpijul::Result<()> {
debug!("switch branch {:?}", branch_name);
let new_repo = libpijul::Repository::open(repo_root.pristine_dir(), Some(increase))?;
let mut txn = new_repo.mut_txn_begin(rand::thread_rng()).unwrap();
let mut branch = if let Some(branch) = txn.get_branch(branch_name) {
branch
} else {
let branch = txn.open_branch(¤t_branch_name)?;
let new_branch = txn.fork(&branch, &branch_name)?;
let _res1 = txn.commit_branch(branch);
let _res2 = txn.commit_branch(new_branch);
txn.open_branch(&branch_name)?
};
use libpijul::ToPrefixes;
let pref = (&[][..] as &[RepoPath<&Path>]).to_prefixes(&txn, &branch);
txn.output_repository(
&mut branch,
&repo_root,
&pref,
&libpijul::patch::UnsignedPatch::empty().leave_unsigned(),
&BTreeSet::new(),
)?;
txn.commit_branch(branch)?;
txn.commit()?;
repo_root.set_current_branch(branch_name)?;
Ok(())
}
fn file_moves(
repo: &Repository,
repo_root: &RepoRoot<PathBuf>,
commit: &Commit<'_>,
pristine_dir: &Path,
increase: u64,
) -> libpijul::Result<Vec<PathBuf>> {
debug!("file_moves, commit {:?}", commit.id());
debug!("commit msg: {:?}", commit.message());
let tree1 = commit.tree().unwrap();
let new_repo = match libpijul::Repository::open(&pristine_dir, Some(increase)) {
Ok(repo) => repo,
Err(x) => return Err(x),
};
let mut txn = new_repo.mut_txn_begin(rand::thread_rng()).unwrap();
let mut has_parents = false;
let mut files = Vec::new();
for parent in commit.parents() {
has_parents = true;
debug!("parent: {:?}", parent.id());
let tree0 = parent.tree().unwrap();
let mut diff = repo
.diff_tree_to_tree(Some(&tree0), Some(&tree1), None)
.unwrap();
diff.find_similar(None).unwrap();
files.extend(
diff.deltas()
.map(&mut |delta| file_cb(&mut txn, delta, repo_root)),
);
}
if !has_parents {
let mut diff = repo.diff_tree_to_tree(None, Some(&tree1), None).unwrap();
diff.find_similar(None).unwrap();
files.extend(
diff.deltas()
.map(&mut |delta| file_cb(&mut txn, delta, repo_root)),
);
}
txn.commit().unwrap();
Ok(files)
}
fn file_cb<'a, 'b, R: Rng>(
txn: &'b mut libpijul::MutTxn<'_, R>,
delta: DiffDelta<'a>,
repo_root: &'b RepoRoot<PathBuf>,
) -> PathBuf {
debug!("nfiles: {:?}", delta.nfiles());
debug!("old: {:?}", delta.old_file().path());
debug!("new: {:?}", delta.new_file().path());
debug!("status {:?}", delta.status());
let old = delta.old_file().path().unwrap();
let new = delta.new_file().path().unwrap();
match delta.status() {
Delta::Renamed => {
debug!("moving {:?} to {:?}", old, new);
txn.move_file(&RepoPath(old), &RepoPath(new), false)
.unwrap();
}
Delta::Added => {
debug!("added {:?}", new);
let m = std::fs::metadata(&repo_root.absolutize(&RepoPath(new))).unwrap();
txn.add_file(&RepoPath(new), m.is_dir()).unwrap_or(())
}
Delta::Deleted => {
debug!("deleted {:?}", new);
txn.remove_file(&RepoPath(new)).unwrap()
}
_ => {}
}
new.to_path_buf()
}
fn record(
repo_root: &RepoRoot<PathBuf>,
branch_name: &str,
header: libpijul::PatchHeader,
files: Vec<PathBuf>,
) {
let (patch, hash, syncs) = {
let mut increase = 409600;
loop {
match patch_no_resize(repo_root, branch_name, &header, &files, increase) {
Err(ref e) if e.lacks_space() => {
debug!("patch_no_resize increase: {:?}", increase);
increase *= 2
}
res => break res.unwrap(),
}
}
};
debug!("hash recorded: {:?}", hash);
let mut increase = 409600;
let pristine_dir = repo_root.pristine_dir();
let res = loop {
match record_no_resize(
&pristine_dir,
&repo_root,
branch_name,
&hash,
&patch,
&syncs,
increase,
) {
Err(ref e) if e.lacks_space() => increase *= 2,
e => break e,
}
};
res.unwrap();
}
fn patch_no_resize(
repo_root: &RepoRoot<PathBuf>,
branch_name: &str,
header: &libpijul::PatchHeader,
files: &Vec<PathBuf>,
increase: u64,
) -> libpijul::Result<(
libpijul::Patch,
libpijul::Hash,
BTreeSet<libpijul::InodeUpdate>,
)> {
let new_repo = libpijul::Repository::open(repo_root.pristine_dir(), Some(increase))?;
let mut new_txn = new_repo.mut_txn_begin(rand::thread_rng())?;
use libpijul::*;
let mut record = RecordState::new();
let mut prefixes = BTreeSet::new();
files.iter().for_each(|file| {
let mut prefix = RepoPath(file.to_path_buf());
loop {
let inode = new_txn.find_inode(&prefix).unwrap();
if let Some(parent) = new_txn.get_revtree(inode) {
if parent.parent_inode.is_root() {
prefixes.insert(prefix);
break;
} else if let Some(_) = new_txn.get_inodes(parent.parent_inode) {
prefixes.insert(prefix);
break;
}
}
prefix = prefix.parent().unwrap().to_owned();
}
});
let mut branch = new_txn.open_branch(branch_name)?;
prefixes.iter().for_each(|prefix| {
debug!("recording: {:?}", prefix);
new_txn
.record(
libpijul::DiffAlgorithm::Myers,
&mut record,
&mut branch,
&repo_root,
&prefix,
)
.unwrap()
});
let (changes, syncs) = record.finish();
let changes: Vec<_> = changes
.into_iter()
.flat_map(|x| new_txn.globalize_record(x).into_iter())
.collect();
let patch = new_txn.new_patch(
&branch,
header.authors.clone(),
header.name.clone(),
header.description.clone(),
header.timestamp.clone(),
changes,
std::iter::empty(), PatchFlags::empty(),
);
let patches_dir = repo_root.patches_dir();
let hash = patch.save(&patches_dir, None)?;
new_txn.commit_branch(branch)?;
new_txn.commit()?;
Ok((patch, hash, syncs))
}
fn record_no_resize(
pristine_dir: &Path,
repo_root: &RepoRoot<PathBuf>,
branch_name: &str,
hash: &libpijul::Hash,
patch: &libpijul::Patch,
syncs: &BTreeSet<libpijul::InodeUpdate>,
increase: u64,
) -> libpijul::Result<Option<libpijul::Hash>> {
use libpijul::*;
let size_increase = increase + patch.size_upper_bound() as u64;
let repo = match Repository::open(&pristine_dir, Some(size_increase)) {
Ok(repo) => repo,
Err(x) => return Err(x),
};
let mut txn = repo.mut_txn_begin(rand::thread_rng())?;
let mut branch = txn.open_branch(branch_name)?;
txn.apply_local_patch(&mut branch, repo_root, &hash, &patch, syncs, false)?;
txn.commit_branch(branch)?;
txn.commit()?;
debug!("Recorded patch {}", hash.to_base58());
Ok(Some(hash.clone()))
}