use std::collections::HashMap;
use std::io::BufRead;
use crate::config::ConfigSet;
use crate::diff::zero_oid;
use crate::error::{Error, Result};
use crate::index::{Index, IndexEntry, MODE_GITLINK, MODE_REGULAR, MODE_TREE};
use crate::objects::{
parse_commit, serialize_commit, serialize_tag, CommitData, ObjectId, ObjectKind, TagData,
};
use crate::refs::{
append_reflog, read_head, resolve_ref, should_autocreate_reflog_for_mode, write_ref,
LogRefsConfig,
};
use crate::repo::Repository;
use crate::rev_parse::resolve_revision;
use crate::write_tree::write_tree_from_index;
#[derive(Debug, Clone, Copy, Default)]
pub struct FastImportOptions {
pub force: bool,
}
pub fn import_stream(repo: &Repository, reader: impl BufRead) -> Result<()> {
import_stream_with_options(repo, reader, FastImportOptions::default())
}
pub fn import_stream_with_options(
repo: &Repository,
mut reader: impl BufRead,
options: FastImportOptions,
) -> Result<()> {
let log_refs = ConfigSet::load(Some(&repo.git_dir), true)
.map(|c| c.effective_log_refs_config(&repo.git_dir))
.unwrap_or_else(|_| crate::refs::effective_log_refs_config(&repo.git_dir));
let mut imp = Importer {
repo,
log_refs,
marks: HashMap::new(),
branch_tips: HashMap::new(),
feature_done: false,
stashed_line: None,
pending_byte: None,
force: options.force,
reader: &mut reader,
};
imp.run()
}
struct Importer<'a, R: BufRead> {
repo: &'a Repository,
log_refs: LogRefsConfig,
marks: HashMap<u32, ObjectId>,
branch_tips: HashMap<String, ObjectId>,
feature_done: bool,
stashed_line: Option<String>,
pending_byte: Option<u8>,
force: bool,
reader: &'a mut R,
}
impl<'a, R: BufRead> Importer<'a, R> {
fn fast_import_reflog_identity_from_env() -> String {
let name = std::env::var("GIT_COMMITTER_NAME").unwrap_or_else(|_| "Unknown".to_owned());
let email = std::env::var("GIT_COMMITTER_EMAIL").unwrap_or_default();
let date = std::env::var("GIT_COMMITTER_DATE").unwrap_or_else(|_| {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
format!("{now} +0000")
});
format!("{name} <{email}> {date}")
}
fn update_ref_with_reflog(
&self,
refname: &str,
new_oid: &ObjectId,
identity: &str,
message: &str,
) -> Result<()> {
let old_oid = resolve_ref(&self.repo.git_dir, refname).unwrap_or_else(|_| zero_oid());
write_ref(&self.repo.git_dir, refname, new_oid)?;
if should_autocreate_reflog_for_mode(refname, self.log_refs) {
let _ = append_reflog(
&self.repo.git_dir,
refname,
&old_oid,
new_oid,
identity,
message,
false,
);
}
Ok(())
}
fn update_ref_for_fast_import(
&self,
refname: &str,
new_oid: &ObjectId,
identity: &str,
message: &str,
) -> Result<()> {
if refname == "HEAD" {
if let Some(target) = read_head(&self.repo.git_dir)? {
if target.starts_with("refs/heads/") {
return self.update_ref_with_reflog(&target, new_oid, identity, message);
}
}
}
self.update_ref_with_reflog(refname, new_oid, identity, message)
}
fn read_data_payload(&mut self, data_line_trimmed: &str) -> Result<Vec<u8>> {
let rest = data_line_trimmed.strip_prefix("data ").ok_or_else(|| {
Error::IndexError(format!(
"fast-import: expected data line, got: {data_line_trimmed}"
))
})?;
if let Some(delim) = rest.strip_prefix("<<") {
let delim = delim.trim_end();
if delim.is_empty() {
return Err(Error::IndexError(
"fast-import: empty data delimiter".to_owned(),
));
}
return self.read_data_delimited(delim);
}
let size: usize = rest
.trim_end()
.parse()
.map_err(|_| Error::IndexError(format!("fast-import: invalid data size: {rest}")))?;
let mut payload = vec![0u8; size];
self.reader
.read_exact(&mut payload)
.map_err(|_| Error::IndexError("fast-import: truncated data".to_owned()))?;
self.consume_optional_lf_after_data()?;
Ok(payload)
}
fn read_data_delimited(&mut self, delim: &str) -> Result<Vec<u8>> {
let mut out = Vec::new();
loop {
let line = self.read_line_any()?.ok_or_else(|| {
Error::IndexError(format!(
"fast-import: EOF in data (terminator '{delim}' not found)"
))
})?;
if line.trim_end() == delim {
break;
}
out.extend_from_slice(line.as_bytes());
}
self.consume_optional_lf_after_data()?;
Ok(out)
}
fn run(&mut self) -> Result<()> {
loop {
let line = match self.next_command_line()? {
Some(l) => l,
None => break,
};
let trimmed = line.trim_end();
if trimmed.is_empty() {
continue;
}
if trimmed == "done" {
break;
}
if let Some(rest) = trimmed.strip_prefix("feature ") {
let name = rest.trim();
if name == "force" {
self.force = true;
} else if name == "done" {
self.feature_done = true;
}
continue;
}
if trimmed.starts_with('#') {
continue;
}
if trimmed == "blob" {
self.read_blob()?;
continue;
}
if let Some(rest) = trimmed.strip_prefix("commit ") {
let refname = rest.trim().to_string();
self.read_commit(&refname)?;
continue;
}
if let Some(rest) = trimmed.strip_prefix("reset ") {
let refname = rest.trim().to_string();
self.read_reset(&refname)?;
continue;
}
if trimmed.starts_with("tag ") {
let name = trimmed["tag ".len()..].trim().to_string();
self.read_tag(&name)?;
continue;
}
return Err(Error::IndexError(format!(
"fast-import: unsupported command: {trimmed}"
)));
}
if self.feature_done {
return Err(Error::IndexError(
"fast-import: stream ended before required \"done\" command".to_owned(),
));
}
Ok(())
}
fn next_command_line(&mut self) -> Result<Option<String>> {
if let Some(l) = self.stashed_line.take() {
return Ok(Some(l));
}
self.read_line_nonempty()
}
fn read_line_nonempty(&mut self) -> Result<Option<String>> {
let mut buf = String::new();
loop {
buf.clear();
let n = self.read_line_into(&mut buf)?;
if n == 0 {
return Ok(None);
}
if !buf.trim().is_empty() {
return Ok(Some(buf));
}
}
}
fn read_line_any(&mut self) -> Result<Option<String>> {
let mut buf = String::new();
let n = self.read_line_into(&mut buf)?;
if n == 0 {
return Ok(None);
}
Ok(Some(buf))
}
fn read_line_into(&mut self, buf: &mut String) -> Result<usize> {
buf.clear();
if let Some(b) = self.pending_byte.take() {
if b == b'\n' {
buf.push('\n');
return Ok(1);
}
buf.push(char::from(b));
}
let prev = buf.len();
let n = self.reader.read_line(buf).map_err(Error::Io)?;
Ok(prev + n)
}
fn read_blob(&mut self) -> Result<()> {
let mut mark: Option<u32> = None;
loop {
let line = self.read_line_nonempty()?.ok_or_else(|| {
Error::IndexError("fast-import: unexpected EOF in blob".to_owned())
})?;
let t = line.trim_end();
if let Some(id) = t.strip_prefix("mark :") {
mark = Some(
id.parse()
.map_err(|_| Error::IndexError(format!("fast-import: bad mark: {t}")))?,
);
continue;
}
if t.starts_with("original-oid ") {
continue;
}
let payload = self.read_data_payload(t)?;
let oid = self.repo.odb.write(ObjectKind::Blob, &payload)?;
if let Some(m) = mark {
self.marks.insert(m, oid);
}
return Ok(());
}
}
fn consume_optional_lf_after_data(&mut self) -> Result<()> {
let mut one = [0u8; 1];
match self.reader.read(&mut one) {
Ok(0) => Ok(()),
Ok(1) => {
if one[0] != b'\n' {
self.pending_byte = Some(one[0]);
}
Ok(())
}
Ok(_) => unreachable!(),
Err(e) => Err(Error::Io(e)),
}
}
fn read_commit(&mut self, refname: &str) -> Result<()> {
let mut mark: Option<u32> = None;
let mut author: Option<String> = None;
let mut committer: Option<String> = None;
loop {
let line = self.read_line_nonempty()?.ok_or_else(|| {
Error::IndexError("fast-import: unexpected EOF in commit".to_owned())
})?;
let t = line.trim_end();
if let Some(id) = t.strip_prefix("mark :") {
mark = Some(
id.parse()
.map_err(|_| Error::IndexError(format!("fast-import: bad mark: {t}")))?,
);
continue;
}
if t.starts_with("original-oid ") {
continue;
}
if let Some(rest) = t.strip_prefix("author ") {
author = Some(rest.to_owned());
continue;
}
if let Some(rest) = t.strip_prefix("committer ") {
committer = Some(rest.to_owned());
continue;
}
if t.starts_with("gpgsig ") || t.starts_with("encoding ") {
return Err(Error::IndexError(format!(
"fast-import: unsupported commit header: {t}"
)));
}
if t.starts_with("data ") {
let message = self.read_data_payload(t)?;
let committer = committer.ok_or_else(|| {
Error::IndexError("fast-import: commit missing committer".to_owned())
})?;
let author = author.unwrap_or_else(|| committer.clone());
self.finish_commit(refname, mark, author, committer, message)?;
return Ok(());
}
return Err(Error::IndexError(format!(
"fast-import: unexpected in commit before message: {t}"
)));
}
}
fn finish_commit(
&mut self,
refname: &str,
mark: Option<u32>,
author: String,
committer: String,
message: Vec<u8>,
) -> Result<()> {
#[derive(Debug)]
enum FileChangeOp {
DeleteAll,
Delete(Vec<u8>),
Modify {
mode: u32,
blob_oid: ObjectId,
path: Vec<u8>,
},
NoteModify {
blob_oid: ObjectId,
target_commit: ObjectId,
},
Rename(Vec<u8>, Vec<u8>),
Copy(Vec<u8>, Vec<u8>),
}
let mut from_oid: Option<ObjectId> = None;
let mut merge_oids: Vec<ObjectId> = Vec::new();
let mut ops: Vec<FileChangeOp> = Vec::new();
let mut pending_inline: Option<(u32, Vec<u8>)> = None;
let notes_ref = refname.starts_with("refs/notes/");
loop {
let Some(line) = self.read_line_any()? else {
break;
};
let t = line.trim_end();
if t.is_empty() {
continue;
}
if let Some((mode, path)) = pending_inline.take() {
if !t.starts_with("data ") {
return Err(Error::IndexError(format!(
"fast-import: expected data after M ... inline, got: {t}"
)));
}
let payload = self.read_data_payload(t)?;
let blob_oid = self.repo.odb.write(ObjectKind::Blob, &payload)?;
ops.push(FileChangeOp::Modify {
mode,
blob_oid,
path,
});
continue;
}
if t.starts_with("from ") {
let spec = t["from ".len()..].trim();
from_oid = Some(self.resolve_commit_ish(spec)?);
continue;
}
if t.starts_with("merge ") {
let spec = t["merge ".len()..].trim();
merge_oids.push(self.resolve_commit_ish(spec)?);
continue;
}
if t == "deleteall" {
ops.push(FileChangeOp::DeleteAll);
continue;
}
if let Some(rest) = t.strip_prefix("M ") {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() < 3 {
return Err(Error::IndexError(format!("fast-import: bad M line: {t}")));
}
let mode = u32::from_str_radix(parts[0], 8).map_err(|_| {
Error::IndexError(format!("fast-import: bad file mode: {}", parts[0]))
})?;
let blob_ref = parts[1];
if parts.len() != 3 {
return Err(Error::IndexError(format!("fast-import: bad M line: {t}")));
}
let path = parts[2].as_bytes().to_vec();
if blob_ref == "inline" {
pending_inline = Some((mode, path));
continue;
}
let blob_oid = self.resolve_blob_ref(blob_ref)?;
ops.push(FileChangeOp::Modify {
mode,
blob_oid,
path,
});
continue;
}
if let Some(rest) = t.strip_prefix("D ") {
ops.push(FileChangeOp::Delete(rest.as_bytes().to_vec()));
continue;
}
if let Some(rest) = t.strip_prefix("N ") {
if !notes_ref {
return Err(Error::IndexError(format!(
"fast-import: N (notemodify) only allowed on refs/notes/*, not {refname}"
)));
}
let (data_ref, commit_spec) = parse_notemodify_operands(rest)?;
let target_commit = self.resolve_note_target_commit(commit_spec)?;
let blob_oid = match data_ref {
NoteBlobSpec::Inline => {
let next = self.read_line_nonempty()?.ok_or_else(|| {
Error::IndexError(
"fast-import: expected data after N inline".to_owned(),
)
})?;
let nt = next.trim_end();
if !nt.starts_with("data ") {
return Err(Error::IndexError(format!(
"fast-import: expected data after N inline, got: {nt}"
)));
}
let payload = self.read_data_payload(nt)?;
self.repo.odb.write(ObjectKind::Blob, &payload)?
}
NoteBlobSpec::Mark(id) => *self.marks.get(&id).ok_or_else(|| {
Error::IndexError(format!("fast-import: unknown mark :{id}"))
})?,
NoteBlobSpec::Oid(oid) => {
if oid.is_zero() {
ObjectId::zero()
} else {
let obj = self.repo.odb.read(&oid)?;
if obj.kind != ObjectKind::Blob {
return Err(Error::IndexError(format!(
"fast-import: N dataref {oid} is not a blob"
)));
}
oid
}
}
};
ops.push(FileChangeOp::NoteModify {
blob_oid,
target_commit,
});
continue;
}
if let Some(rest) = t.strip_prefix("R ") {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() != 2 {
return Err(Error::IndexError(format!("fast-import: bad R line: {t}")));
}
ops.push(FileChangeOp::Rename(
parts[0].as_bytes().to_vec(),
parts[1].as_bytes().to_vec(),
));
continue;
}
if let Some(rest) = t.strip_prefix("C ") {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() != 2 {
return Err(Error::IndexError(format!("fast-import: bad C line: {t}")));
}
ops.push(FileChangeOp::Copy(
parts[0].as_bytes().to_vec(),
parts[1].as_bytes().to_vec(),
));
continue;
}
self.stashed_line = Some(line);
break;
}
if pending_inline.is_some() {
return Err(Error::IndexError(
"fast-import: unterminated M ... inline (missing data)".to_owned(),
));
}
let empty_tree: ObjectId = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
.parse()
.map_err(|_| Error::IndexError("fast-import: empty tree oid".to_owned()))?;
let mut parents: Vec<ObjectId> = Vec::new();
if let Some(oid) = from_oid {
parents.push(oid);
}
parents.extend(merge_oids);
let (parent_tree, parents_for_commit) = if let Some(&first_parent) = parents.first() {
let obj = self.repo.odb.read(&first_parent)?;
if obj.kind != ObjectKind::Commit {
return Err(Error::IndexError(format!(
"fast-import: parent {first_parent} is not a commit"
)));
}
let c = parse_commit(&obj.data)?;
(c.tree, parents)
} else if let Some(tip) = self.branch_tips.get(refname).copied() {
let obj = self.repo.odb.read(&tip)?;
if obj.kind != ObjectKind::Commit {
return Err(Error::IndexError(format!(
"fast-import: branch tip {tip} is not a commit"
)));
}
let c = parse_commit(&obj.data)?;
(c.tree, vec![tip])
} else {
(empty_tree, Vec::new())
};
let mut index = tree_to_index(&self.repo.odb, &parent_tree)?;
for op in ops {
match op {
FileChangeOp::DeleteAll => index.entries.clear(),
FileChangeOp::Delete(path) => {
index.entries.retain(|e| e.path != path);
}
FileChangeOp::Modify {
mode,
blob_oid,
path,
} => {
let mode = normalize_mode(mode)?;
index.add_or_replace(index_entry(path, mode, blob_oid));
}
FileChangeOp::NoteModify {
blob_oid,
target_commit,
} => {
remove_note_entries_for_target(&mut index, &target_commit);
if !blob_oid.is_zero() {
let after_remove = count_notes_in_index(&index);
let fanout = notes_fanout_for_count(after_remove.saturating_add(1));
let note_path = construct_note_path_with_fanout(&target_commit, fanout);
index.add_or_replace(index_entry(note_path, MODE_REGULAR, blob_oid));
}
}
FileChangeOp::Rename(src, dst) => {
let Some(pos) = index.entries.iter().position(|e| e.path == src) else {
return Err(Error::IndexError(format!(
"fast-import: filerename source missing: {}",
String::from_utf8_lossy(&src)
)));
};
let mut ent = index.entries.remove(pos);
ent.path = dst;
index.add_or_replace(ent);
}
FileChangeOp::Copy(src, dst) => {
let Some(ent) = index.entries.iter().find(|e| e.path == src).cloned() else {
return Err(Error::IndexError(format!(
"fast-import: filecopy source missing: {}",
String::from_utf8_lossy(&src)
)));
};
let mut copy_ent = ent;
copy_ent.path = dst;
index.add_or_replace(copy_ent);
}
}
}
if notes_ref && count_notes_in_index(&index) > 0 {
let n = count_notes_in_index(&index);
rewrite_notes_fanout_in_index(&mut index, notes_fanout_for_count(n))?;
}
let tree_oid = write_tree_from_index(&self.repo.odb, &index, "")?;
let message_str = String::from_utf8_lossy(&message).into_owned();
let raw_message = (!message.is_empty() && std::str::from_utf8(&message).is_err())
.then_some(message.clone());
let reflog_identity = committer.clone();
let commit = CommitData {
tree: tree_oid,
parents: parents_for_commit,
author,
committer,
author_raw: Vec::new(),
committer_raw: Vec::new(),
encoding: None,
message: message_str,
raw_message,
};
let bytes = serialize_commit(&commit);
let commit_oid = self.repo.odb.write(ObjectKind::Commit, &bytes)?;
if let Some(m) = mark {
self.marks.insert(m, commit_oid);
}
self.branch_tips.insert(refname.to_string(), commit_oid);
if !self.force {
if let Ok(old) = crate::refs::resolve_ref(&self.repo.git_dir, refname) {
if old != commit_oid {
let is_ancestor =
crate::merge_base::is_ancestor(self.repo, old, commit_oid).unwrap_or(false);
if !is_ancestor {
return Err(Error::IndexError(format!(
"fast-import: refusing non-fast-forward update of {refname} (use feature force or --force)"
)));
}
}
}
}
self.update_ref_for_fast_import(refname, &commit_oid, &reflog_identity, "fast-import")?;
Ok(())
}
fn resolve_commit_ish(&self, spec: &str) -> Result<ObjectId> {
if let Some(rest) = spec.strip_prefix(':') {
let id: u32 = rest
.parse()
.map_err(|_| Error::IndexError(format!("fast-import: bad mark ref: {spec}")))?;
return self
.marks
.get(&id)
.copied()
.ok_or_else(|| Error::IndexError(format!("fast-import: unknown mark :{id}")));
}
if spec.len() == 40 && spec.chars().all(|c| c.is_ascii_hexdigit()) {
return spec.parse();
}
resolve_revision(self.repo, spec)
}
fn resolve_blob_ref(&self, spec: &str) -> Result<ObjectId> {
if let Some(rest) = spec.strip_prefix(':') {
let id: u32 = rest
.parse()
.map_err(|_| Error::IndexError(format!("fast-import: bad mark ref: {spec}")))?;
return self
.marks
.get(&id)
.copied()
.ok_or_else(|| Error::IndexError(format!("fast-import: unknown mark :{id}")));
}
if spec.len() == 40 && spec.chars().all(|c| c.is_ascii_hexdigit()) {
return spec.parse();
}
Err(Error::IndexError(format!(
"fast-import: unsupported blob ref: {spec}"
)))
}
fn read_tag(&mut self, short_name: &str) -> Result<()> {
let mut mark: Option<u32> = None;
let mut from_oid: Option<ObjectId> = None;
let mut tagger: Option<String> = None;
loop {
let line = self.read_line_nonempty()?.ok_or_else(|| {
Error::IndexError("fast-import: unexpected EOF in tag".to_owned())
})?;
let t = line.trim_end();
if let Some(id) = t.strip_prefix("mark :") {
mark = Some(
id.parse()
.map_err(|_| Error::IndexError(format!("fast-import: bad mark: {t}")))?,
);
continue;
}
if t.starts_with("original-oid ") {
continue;
}
if let Some(rest) = t.strip_prefix("from ") {
let spec = rest.trim();
from_oid = Some(self.resolve_commit_ish(spec)?);
continue;
}
if let Some(rest) = t.strip_prefix("tagger ") {
tagger = Some(rest.to_owned());
continue;
}
if t.starts_with("data ") {
let message = self.read_data_payload(t)?;
let target = from_oid
.ok_or_else(|| Error::IndexError("fast-import: tag missing from".to_owned()))?;
let target_obj = self.repo.odb.read(&target)?;
let object_type = target_obj.kind.as_str().to_owned();
let msg_str = String::from_utf8_lossy(&message).into_owned();
let reflog_ident = tagger
.clone()
.unwrap_or_else(Self::fast_import_reflog_identity_from_env);
let tag_data = TagData {
object: target,
object_type,
tag: short_name.to_owned(),
tagger,
message: msg_str,
};
let bytes = serialize_tag(&tag_data);
let tag_oid = self.repo.odb.write(ObjectKind::Tag, &bytes)?;
if let Some(m) = mark {
self.marks.insert(m, tag_oid);
}
let full_ref = format!("refs/tags/{short_name}");
self.update_ref_with_reflog(&full_ref, &tag_oid, &reflog_ident, "fast-import")?;
return Ok(());
}
return Err(Error::IndexError(format!(
"fast-import: unexpected in tag: {t}"
)));
}
}
fn read_reset(&mut self, refname: &str) -> Result<()> {
let Some(line) = self.read_line_any()? else {
return Ok(());
};
let t = line.trim_end();
if t.is_empty() {
return Ok(());
}
if let Some(spec) = t.strip_prefix("from ") {
let oid = self.resolve_commit_ish(spec.trim())?;
self.branch_tips.insert(refname.to_string(), oid);
if !self.force {
if let Ok(old) = crate::refs::resolve_ref(&self.repo.git_dir, refname) {
if old != oid {
let is_ancestor =
crate::merge_base::is_ancestor(self.repo, old, oid).unwrap_or(false);
if !is_ancestor {
return Err(Error::IndexError(format!(
"fast-import: refusing non-fast-forward reset of {refname}"
)));
}
}
}
}
let ident = Self::fast_import_reflog_identity_from_env();
self.update_ref_for_fast_import(refname, &oid, &ident, "fast-import")?;
return Ok(());
}
self.stashed_line = Some(line);
Ok(())
}
fn resolve_note_target_commit(&self, spec: &str) -> Result<ObjectId> {
let oid = if let Some(tip) = self.branch_tips.get(spec) {
*tip
} else {
self.resolve_commit_ish(spec)?
};
let obj = self.repo.odb.read(&oid)?;
if obj.kind != ObjectKind::Commit {
return Err(Error::IndexError(format!(
"fast-import: notemodify target {spec} is not a commit"
)));
}
Ok(oid)
}
}
enum NoteBlobSpec {
Inline,
Mark(u32),
Oid(ObjectId),
}
fn parse_notemodify_operands(rest: &str) -> Result<(NoteBlobSpec, &str)> {
let s = rest.trim();
if let Some(commit_spec) = s.strip_prefix("inline ") {
return Ok((NoteBlobSpec::Inline, commit_spec.trim()));
}
if let Some(after_colon) = s.strip_prefix(':') {
let space = after_colon
.find(' ')
.ok_or_else(|| Error::IndexError("fast-import: bad N line (mark)".to_owned()))?;
let id: u32 = after_colon[..space]
.parse()
.map_err(|_| Error::IndexError(format!("fast-import: bad mark in N line: {s}")))?;
return Ok((NoteBlobSpec::Mark(id), after_colon[space + 1..].trim()));
}
if s.len() < 41 {
return Err(Error::IndexError(format!(
"fast-import: bad N line (expected oid + commit-ish): {s}"
)));
}
let head = &s[..40];
if !head.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(Error::IndexError(format!(
"fast-import: bad N line (invalid blob oid): {s}"
)));
}
if s.as_bytes().get(40) != Some(&b' ') {
return Err(Error::IndexError(format!(
"fast-import: bad N line (missing space after blob oid): {s}"
)));
}
let oid: ObjectId = head
.parse()
.map_err(|_| Error::IndexError(format!("fast-import: bad blob oid in N line: {s}")))?;
Ok((NoteBlobSpec::Oid(oid), s[41..].trim()))
}
fn is_note_index_path(path: &[u8]) -> bool {
let compact: Vec<u8> = path.iter().copied().filter(|b| *b != b'/').collect();
compact.len() == 40 && compact.iter().all(u8::is_ascii_hexdigit)
}
fn compact_hex_from_note_path(path: &[u8]) -> Option<String> {
if !is_note_index_path(path) {
return None;
}
let s: String = path
.iter()
.copied()
.filter(|b| *b != b'/')
.map(|b| char::from(b).to_ascii_lowercase())
.collect();
Some(s)
}
fn count_notes_in_index(index: &crate::index::Index) -> usize {
index
.entries
.iter()
.filter(|e| is_note_index_path(&e.path))
.count()
}
fn notes_fanout_for_count(mut n: usize) -> usize {
let mut fanout = 0usize;
while n > 0xff {
n >>= 8;
fanout += 1;
}
fanout
}
fn construct_note_path_with_fanout(commit: &ObjectId, fanout: usize) -> Vec<u8> {
let hex = commit.to_hex();
let bytes = hex.as_bytes();
let split = fanout.min(bytes.len() / 2);
let mut out = Vec::with_capacity(hex.len() + split);
for i in 0..split {
let start = i * 2;
out.extend_from_slice(&bytes[start..start + 2]);
out.push(b'/');
}
out.extend_from_slice(&bytes[split * 2..]);
out
}
fn remove_note_entries_for_target(index: &mut crate::index::Index, target: &ObjectId) {
let want = target.to_hex();
index.entries.retain(|e| {
if !is_note_index_path(&e.path) {
return true;
}
compact_hex_from_note_path(&e.path).as_deref() != Some(want.as_str())
});
}
fn rewrite_notes_fanout_in_index(index: &mut crate::index::Index, fanout: usize) -> Result<()> {
let mut notes: Vec<(ObjectId, ObjectId, u32)> = Vec::new();
let mut kept = Vec::new();
for e in index.entries.drain(..) {
if is_note_index_path(&e.path) {
let Some(compact) = compact_hex_from_note_path(&e.path) else {
continue;
};
let commit_oid = compact
.parse()
.map_err(|_| Error::IndexError("fast-import: bad note path in index".to_owned()))?;
notes.push((commit_oid, e.oid, e.mode));
} else {
kept.push(e);
}
}
index.entries = kept;
for (commit_oid, blob_oid, mode) in notes {
let path = construct_note_path_with_fanout(&commit_oid, fanout);
index.add_or_replace(index_entry(path, mode, blob_oid));
}
Ok(())
}
fn normalize_mode(mode: u32) -> Result<u32> {
match mode {
0o100644 | 0o644 => Ok(MODE_REGULAR),
0o100755 | 0o755 => Ok(crate::index::MODE_EXECUTABLE),
0o120000 => Ok(crate::index::MODE_SYMLINK),
0o160000 => Ok(MODE_GITLINK),
0o040000 => Ok(MODE_TREE),
_ => Err(Error::IndexError(format!(
"fast-import: unsupported mode {mode:o}"
))),
}
}
fn index_entry(path: Vec<u8>, mode: u32, oid: ObjectId) -> IndexEntry {
let path_len = path.len().min(0xFFF) as u16;
IndexEntry {
ctime_sec: 0,
ctime_nsec: 0,
mtime_sec: 0,
mtime_nsec: 0,
dev: 0,
ino: 0,
mode,
uid: 0,
gid: 0,
size: 0,
oid,
flags: path_len,
flags_extended: Some(0),
path,
base_index_pos: 0,
}
}
fn tree_to_index(odb: &crate::odb::Odb, tree_oid: &ObjectId) -> Result<Index> {
let obj = odb.read(tree_oid)?;
if obj.kind != ObjectKind::Tree {
return Err(Error::IndexError(format!("expected tree at {tree_oid}")));
}
let entries = crate::objects::parse_tree(&obj.data)?;
let mut index = Index::new();
for te in entries {
let path = te.name;
if te.mode == MODE_TREE {
let sub = tree_to_index(odb, &te.oid)?;
for mut e in sub.entries {
let mut full = path.clone();
full.push(b'/');
full.extend_from_slice(&e.path);
e.path = full;
let pl = e.path.len().min(0xFFF) as u16;
e.flags = pl;
index.add_or_replace(e);
}
} else {
index.add_or_replace(index_entry(path, te.mode, te.oid));
}
}
Ok(index)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::refs::resolve_ref;
use crate::repo::init_repository;
use std::io::Cursor;
use tempfile::tempdir;
#[test]
fn fast_import_delimited_data_m_inline_and_note() -> Result<()> {
let dir =
tempdir().map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
let repo = init_repository(dir.path(), false, "main", None, "files")?;
let setup = r#"commit refs/heads/main
committer T <t@e> 1000000000 +0000
data <<COMMIT
m1
COMMIT
M 644 inline f
data <<EOF
a
EOF
commit refs/heads/main
committer T <t@e> 1000000001 +0000
data <<COMMIT
m2
COMMIT
M 644 inline f
data <<EOF
b
EOF
"#;
import_stream(&repo, Cursor::new(setup.as_bytes()))?;
let c2 = resolve_ref(&repo.git_dir, "refs/heads/main")?;
let c2_obj = repo.odb.read(&c2)?;
let c2_parsed = parse_commit(&c2_obj.data)?;
let c1 = c2_parsed
.parents
.first()
.copied()
.ok_or_else(|| Error::IndexError("test: expected parent commit".to_owned()))?;
let notes = format!(
r#"commit refs/notes/commits
committer T <t@e> 1000000002 +0000
data <<COMMIT
n1
COMMIT
N inline {c1}
data <<EOF
note1
EOF
N inline {c2}
data <<EOF
note2
EOF
commit refs/notes/commits
committer T <t@e> 1000000003 +0000
data <<COMMIT
n2
COMMIT
M 644 inline foobar/x.txt
data <<EOF
non-note
EOF
N inline {c2}
data <<EOF
edited
EOF
"#
);
import_stream(&repo, Cursor::new(notes.as_bytes()))?;
let notes_tip = resolve_ref(&repo.git_dir, "refs/notes/commits")?;
let commit_obj = repo.odb.read(¬es_tip)?;
let parsed = parse_commit(&commit_obj.data)?;
let tree = tree_to_index(&repo.odb, &parsed.tree)?;
assert!(
tree.entries.iter().any(|e| e.path == b"foobar/x.txt"),
"expected non-note path preserved"
);
let mut found_edit = false;
for e in &tree.entries {
if is_note_index_path(&e.path) {
let compact = compact_hex_from_note_path(&e.path).expect("note path");
if compact == c2.to_hex() {
let blob = repo.odb.read(&e.oid)?;
assert_eq!(blob.data, b"edited\n");
found_edit = true;
}
}
}
assert!(found_edit, "expected edited note for second commit");
Ok(())
}
}