use std::path::PathBuf;
use panproto_project::ProjectBuilder;
use panproto_vcs::{CommitObject, Object, ObjectId, Store};
use crate::error::GitBridgeError;
#[derive(Debug)]
pub struct ImportResult {
pub commit_count: usize,
pub head_id: ObjectId,
pub oid_map: Vec<(git2::Oid, ObjectId)>,
}
pub fn import_git_repo<S: Store>(
git_repo: &git2::Repository,
panproto_store: &mut S,
revspec: &str,
) -> Result<ImportResult, GitBridgeError> {
let obj = git_repo.revparse_single(revspec)?;
let head_commit = obj
.peel_to_commit()
.map_err(|e| GitBridgeError::ObjectRead {
oid: obj.id().to_string(),
reason: format!("not a commit: {e}"),
})?;
let mut commits = Vec::new();
collect_ancestors(git_repo, head_commit.id(), &mut commits)?;
let mut oid_map: Vec<(git2::Oid, ObjectId)> = Vec::new();
let mut git_to_panproto: rustc_hash::FxHashMap<git2::Oid, ObjectId> =
rustc_hash::FxHashMap::default();
let mut last_id = ObjectId::ZERO;
for git_oid in &commits {
let git_commit = git_repo.find_commit(*git_oid)?;
let tree = git_commit.tree()?;
let mut project_builder = ProjectBuilder::new();
walk_git_tree(git_repo, &tree, &PathBuf::new(), &mut project_builder)?;
let project = if project_builder.file_count() == 0 {
let proto = panproto_protocols::raw_file::protocol();
let builder = panproto_schema::SchemaBuilder::new(&proto);
builder
.vertex("root", "file", None)
.map_err(|e| {
GitBridgeError::Project(panproto_project::ProjectError::CoproductFailed {
reason: format!("empty tree schema: {e}"),
})
})?
.build()
.map_err(|e| {
GitBridgeError::Project(panproto_project::ProjectError::CoproductFailed {
reason: format!("empty tree build: {e}"),
})
})?
} else {
project_builder.build()?.schema
};
let schema_id = panproto_store.put(&Object::Schema(Box::new(project)))?;
let parents: Vec<ObjectId> = git_commit
.parent_ids()
.filter_map(|parent_oid| git_to_panproto.get(&parent_oid).copied())
.collect();
let author_sig = git_commit.author();
let author = author_sig.name().unwrap_or("unknown").to_owned();
let timestamp = u64::try_from(author_sig.when().seconds()).unwrap_or(0);
let message = git_commit.message().unwrap_or("(no message)").to_owned();
let commit = CommitObject {
schema_id,
parents,
migration_id: None,
protocol: "project".to_owned(),
author,
timestamp,
message,
renames: Vec::new(),
protocol_id: None,
data_ids: Vec::new(),
complement_ids: Vec::new(),
edit_log_ids: Vec::new(),
};
let commit_id = panproto_store.put(&Object::Commit(commit))?;
git_to_panproto.insert(*git_oid, commit_id);
oid_map.push((*git_oid, commit_id));
last_id = commit_id;
}
if !commits.is_empty() {
panproto_store.set_ref("refs/heads/main", last_id)?;
}
Ok(ImportResult {
commit_count: commits.len(),
head_id: last_id,
oid_map,
})
}
fn collect_ancestors(
repo: &git2::Repository,
head: git2::Oid,
result: &mut Vec<git2::Oid>,
) -> Result<(), GitBridgeError> {
let mut revwalk = repo.revwalk()?;
revwalk.push(head)?;
revwalk.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::REVERSE)?;
for oid_result in revwalk {
result.push(oid_result?);
}
Ok(())
}
fn walk_git_tree(
repo: &git2::Repository,
tree: &git2::Tree<'_>,
prefix: &std::path::Path,
builder: &mut ProjectBuilder,
) -> Result<(), GitBridgeError> {
for entry in tree {
let name = entry.name().unwrap_or("(unnamed)");
let path = prefix.join(name);
match entry.kind() {
Some(git2::ObjectType::Blob) => {
let blob = repo.find_blob(entry.id())?;
let content = blob.content();
builder.add_file(&path, content)?;
}
Some(git2::ObjectType::Tree) => {
let subtree = repo.find_tree(entry.id())?;
walk_git_tree(repo, &subtree, &path, builder)?;
}
_ => {
}
}
}
Ok(())
}