1use std::collections::HashMap;
10use std::io::BufRead;
11
12use crate::check_ref_format::{check_refname_format, RefNameOptions};
13use crate::config::ConfigSet;
14use crate::diff::zero_oid;
15use crate::error::{Error, Result};
16use crate::index::{Index, IndexEntry, MODE_GITLINK, MODE_REGULAR, MODE_TREE};
17use crate::objects::{
18 parse_commit, serialize_commit, serialize_tag, CommitData, ObjectId, ObjectKind, TagData,
19};
20use crate::refs::{
21 append_reflog, read_head, resolve_ref, should_autocreate_reflog_for_mode, write_ref,
22 LogRefsConfig,
23};
24use crate::repo::Repository;
25use crate::rev_parse::resolve_revision;
26use crate::write_tree::write_tree_from_index;
27
28#[derive(Debug, Clone, Copy, Default)]
30pub struct FastImportOptions {
31 pub force: bool,
34}
35
36pub fn import_stream(repo: &Repository, reader: impl BufRead) -> Result<()> {
42 import_stream_with_options(repo, reader, FastImportOptions::default())
43}
44
45pub fn import_stream_with_options(
47 repo: &Repository,
48 mut reader: impl BufRead,
49 options: FastImportOptions,
50) -> Result<()> {
51 let log_refs = ConfigSet::load(Some(&repo.git_dir), true)
52 .map(|c| c.effective_log_refs_config(&repo.git_dir))
53 .unwrap_or_else(|_| crate::refs::effective_log_refs_config(&repo.git_dir));
54 let mut imp = Importer {
55 repo,
56 log_refs,
57 marks: HashMap::new(),
58 branch_tips: HashMap::new(),
59 feature_done: false,
60 stashed_line: None,
61 pending_byte: None,
62 force: options.force,
63 reader: &mut reader,
64 };
65 imp.run()
66}
67
68struct Importer<'a, R: BufRead> {
69 repo: &'a Repository,
70 log_refs: LogRefsConfig,
71 marks: HashMap<u32, ObjectId>,
72 branch_tips: HashMap<String, ObjectId>,
73 feature_done: bool,
75 stashed_line: Option<String>,
77 pending_byte: Option<u8>,
79 force: bool,
80 reader: &'a mut R,
81}
82
83impl<'a, R: BufRead> Importer<'a, R> {
84 fn fast_import_reflog_identity_from_env() -> String {
85 let name = std::env::var("GIT_COMMITTER_NAME").unwrap_or_else(|_| "Unknown".to_owned());
86 let email = std::env::var("GIT_COMMITTER_EMAIL").unwrap_or_default();
87 let date = std::env::var("GIT_COMMITTER_DATE").unwrap_or_else(|_| {
88 let now = std::time::SystemTime::now()
89 .duration_since(std::time::UNIX_EPOCH)
90 .map(|d| d.as_secs())
91 .unwrap_or(0);
92 format!("{now} +0000")
93 });
94 format!("{name} <{email}> {date}")
95 }
96
97 fn update_ref_with_reflog(
99 &self,
100 refname: &str,
101 new_oid: &ObjectId,
102 identity: &str,
103 message: &str,
104 ) -> Result<()> {
105 let old_oid = resolve_ref(&self.repo.git_dir, refname).unwrap_or_else(|_| zero_oid());
106 write_ref(&self.repo.git_dir, refname, new_oid)?;
107 if should_autocreate_reflog_for_mode(refname, self.log_refs) {
108 let _ = append_reflog(
109 &self.repo.git_dir,
110 refname,
111 &old_oid,
112 new_oid,
113 identity,
114 message,
115 false,
116 );
117 }
118 Ok(())
119 }
120
121 fn update_ref_for_fast_import(
128 &self,
129 refname: &str,
130 new_oid: &ObjectId,
131 identity: &str,
132 message: &str,
133 ) -> Result<()> {
134 if refname == "HEAD" {
135 if let Some(target) = read_head(&self.repo.git_dir)? {
136 if target.starts_with("refs/heads/") {
137 return self.update_ref_with_reflog(&target, new_oid, identity, message);
138 }
139 }
140 }
141 self.update_ref_with_reflog(refname, new_oid, identity, message)
142 }
143
144 fn read_data_payload(&mut self, data_line_trimmed: &str) -> Result<Vec<u8>> {
146 let rest = data_line_trimmed.strip_prefix("data ").ok_or_else(|| {
147 Error::IndexError(format!(
148 "fast-import: expected data line, got: {data_line_trimmed}"
149 ))
150 })?;
151 if let Some(delim) = rest.strip_prefix("<<") {
152 let delim = delim.trim_end();
153 if delim.is_empty() {
154 return Err(Error::IndexError(
155 "fast-import: empty data delimiter".to_owned(),
156 ));
157 }
158 return self.read_data_delimited(delim);
159 }
160 let size: usize = rest
161 .trim_end()
162 .parse()
163 .map_err(|_| Error::IndexError(format!("fast-import: invalid data size: {rest}")))?;
164 let mut payload = vec![0u8; size];
165 self.reader
166 .read_exact(&mut payload)
167 .map_err(|_| Error::IndexError("fast-import: truncated data".to_owned()))?;
168 self.consume_optional_lf_after_data()?;
169 Ok(payload)
170 }
171
172 fn read_data_delimited(&mut self, delim: &str) -> Result<Vec<u8>> {
174 let mut out = Vec::new();
175 loop {
176 let line = self.read_line_any()?.ok_or_else(|| {
177 Error::IndexError(format!(
178 "fast-import: EOF in data (terminator '{delim}' not found)"
179 ))
180 })?;
181 if line.trim_end() == delim {
182 break;
183 }
184 out.extend_from_slice(line.as_bytes());
185 }
186 self.consume_optional_lf_after_data()?;
187 Ok(out)
188 }
189
190 fn run(&mut self) -> Result<()> {
191 loop {
192 let line = match self.next_command_line()? {
193 Some(l) => l,
194 None => break,
195 };
196 let trimmed = line.trim_end();
197 if trimmed.is_empty() {
198 continue;
199 }
200 if trimmed == "done" {
201 break;
202 }
203 if let Some(rest) = trimmed.strip_prefix("feature ") {
204 let name = rest.trim();
205 if name == "force" {
206 self.force = true;
207 } else if name == "done" {
208 self.feature_done = true;
209 }
210 continue;
211 }
212 if trimmed.starts_with('#') {
213 continue;
214 }
215 if trimmed == "blob" {
216 self.read_blob()?;
217 continue;
218 }
219 if let Some(rest) = trimmed.strip_prefix("commit ") {
220 let refname = rest.trim().to_string();
221 validate_fast_import_refname(&refname)?;
222 self.read_commit(&refname)?;
223 continue;
224 }
225 if let Some(rest) = trimmed.strip_prefix("reset ") {
226 let refname = rest.trim().to_string();
227 validate_fast_import_refname(&refname)?;
228 self.read_reset(&refname)?;
229 continue;
230 }
231 if trimmed.starts_with("tag ") {
232 let name = trimmed["tag ".len()..].trim().to_string();
233 self.read_tag(&name)?;
234 continue;
235 }
236 return Err(Error::IndexError(format!(
237 "fast-import: unsupported command: {trimmed}"
238 )));
239 }
240 if self.feature_done {
241 return Err(Error::IndexError(
242 "fast-import: stream ended before required \"done\" command".to_owned(),
243 ));
244 }
245 Ok(())
246 }
247
248 fn next_command_line(&mut self) -> Result<Option<String>> {
249 if let Some(l) = self.stashed_line.take() {
250 return Ok(Some(l));
251 }
252 self.read_line_nonempty()
253 }
254
255 fn read_line_nonempty(&mut self) -> Result<Option<String>> {
256 let mut buf = String::new();
257 loop {
258 buf.clear();
259 let n = self.read_line_into(&mut buf)?;
260 if n == 0 {
261 return Ok(None);
262 }
263 if !buf.trim().is_empty() {
264 return Ok(Some(buf));
265 }
266 }
267 }
268
269 fn read_line_any(&mut self) -> Result<Option<String>> {
270 let mut buf = String::new();
271 let n = self.read_line_into(&mut buf)?;
272 if n == 0 {
273 return Ok(None);
274 }
275 Ok(Some(buf))
276 }
277
278 fn read_line_into(&mut self, buf: &mut String) -> Result<usize> {
279 buf.clear();
280 if let Some(b) = self.pending_byte.take() {
281 if b == b'\n' {
282 buf.push('\n');
283 return Ok(1);
284 }
285 buf.push(char::from(b));
286 }
287 let prev = buf.len();
288 let n = self.reader.read_line(buf).map_err(Error::Io)?;
289 Ok(prev + n)
290 }
291
292 fn read_blob(&mut self) -> Result<()> {
293 let mut mark: Option<u32> = None;
294 loop {
295 let line = self.read_line_nonempty()?.ok_or_else(|| {
296 Error::IndexError("fast-import: unexpected EOF in blob".to_owned())
297 })?;
298 let t = line.trim_end();
299 if let Some(id) = t.strip_prefix("mark :") {
300 mark = Some(
301 id.parse()
302 .map_err(|_| Error::IndexError(format!("fast-import: bad mark: {t}")))?,
303 );
304 continue;
305 }
306 if t.starts_with("original-oid ") {
307 continue;
308 }
309 let payload = self.read_data_payload(t)?;
310 let oid = self.repo.odb.write(ObjectKind::Blob, &payload)?;
311 if let Some(m) = mark {
312 self.marks.insert(m, oid);
313 }
314 return Ok(());
315 }
316 }
317
318 fn consume_optional_lf_after_data(&mut self) -> Result<()> {
320 let mut one = [0u8; 1];
321 match self.reader.read(&mut one) {
322 Ok(0) => Ok(()),
323 Ok(1) => {
324 if one[0] != b'\n' {
325 self.pending_byte = Some(one[0]);
326 }
327 Ok(())
328 }
329 Ok(_) => unreachable!(),
330 Err(e) => Err(Error::Io(e)),
331 }
332 }
333
334 fn read_commit(&mut self, refname: &str) -> Result<()> {
335 let mut mark: Option<u32> = None;
336 let mut author: Option<String> = None;
337 let mut committer: Option<String> = None;
338
339 loop {
340 let line = self.read_line_nonempty()?.ok_or_else(|| {
341 Error::IndexError("fast-import: unexpected EOF in commit".to_owned())
342 })?;
343 let t = line.trim_end();
344 if let Some(id) = t.strip_prefix("mark :") {
345 mark = Some(
346 id.parse()
347 .map_err(|_| Error::IndexError(format!("fast-import: bad mark: {t}")))?,
348 );
349 continue;
350 }
351 if t.starts_with("original-oid ") {
352 continue;
353 }
354 if let Some(rest) = t.strip_prefix("author ") {
355 author = Some(rest.to_owned());
356 continue;
357 }
358 if let Some(rest) = t.strip_prefix("committer ") {
359 committer = Some(rest.to_owned());
360 continue;
361 }
362 if t.starts_with("gpgsig ") || t.starts_with("encoding ") {
363 return Err(Error::IndexError(format!(
364 "fast-import: unsupported commit header: {t}"
365 )));
366 }
367 if t.starts_with("data ") {
368 let message = self.read_data_payload(t)?;
369 let committer = committer.ok_or_else(|| {
370 Error::IndexError("fast-import: commit missing committer".to_owned())
371 })?;
372 let author = author.unwrap_or_else(|| committer.clone());
373 self.finish_commit(refname, mark, author, committer, message)?;
374 return Ok(());
375 }
376 return Err(Error::IndexError(format!(
377 "fast-import: unexpected in commit before message: {t}"
378 )));
379 }
380 }
381
382 fn finish_commit(
383 &mut self,
384 refname: &str,
385 mark: Option<u32>,
386 author: String,
387 committer: String,
388 message: Vec<u8>,
389 ) -> Result<()> {
390 #[derive(Debug)]
391 enum FileChangeOp {
392 DeleteAll,
393 Delete(Vec<u8>),
394 Modify {
395 mode: u32,
396 blob_oid: ObjectId,
397 path: Vec<u8>,
398 },
399 NoteModify {
400 blob_oid: ObjectId,
401 target_commit: ObjectId,
402 },
403 Rename(Vec<u8>, Vec<u8>),
404 Copy(Vec<u8>, Vec<u8>),
405 }
406
407 let mut from_oid: Option<ObjectId> = None;
408 let mut merge_oids: Vec<ObjectId> = Vec::new();
409 let mut ops: Vec<FileChangeOp> = Vec::new();
410 let mut pending_inline: Option<(u32, Vec<u8>)> = None;
411 let notes_ref = refname.starts_with("refs/notes/");
412
413 loop {
414 let Some(line) = self.read_line_any()? else {
415 break;
416 };
417 let t = line.trim_end();
418 if t.is_empty() {
419 continue;
420 }
421 if let Some((mode, path)) = pending_inline.take() {
422 if !t.starts_with("data ") {
423 return Err(Error::IndexError(format!(
424 "fast-import: expected data after M ... inline, got: {t}"
425 )));
426 }
427 let payload = self.read_data_payload(t)?;
428 let blob_oid = self.repo.odb.write(ObjectKind::Blob, &payload)?;
429 ops.push(FileChangeOp::Modify {
430 mode,
431 blob_oid,
432 path,
433 });
434 continue;
435 }
436 if t.starts_with("from ") {
437 let spec = t["from ".len()..].trim();
438 from_oid = Some(self.resolve_commit_ish(spec)?);
439 continue;
440 }
441 if t.starts_with("merge ") {
442 let spec = t["merge ".len()..].trim();
443 merge_oids.push(self.resolve_commit_ish(spec)?);
444 continue;
445 }
446 if t == "deleteall" {
447 ops.push(FileChangeOp::DeleteAll);
448 continue;
449 }
450 if let Some(rest) = t.strip_prefix("M ") {
451 let parts: Vec<&str> = rest.split_whitespace().collect();
452 if parts.len() < 3 {
453 return Err(Error::IndexError(format!("fast-import: bad M line: {t}")));
454 }
455 let mode = u32::from_str_radix(parts[0], 8).map_err(|_| {
456 Error::IndexError(format!("fast-import: bad file mode: {}", parts[0]))
457 })?;
458 let blob_ref = parts[1];
459 if parts.len() != 3 {
460 return Err(Error::IndexError(format!("fast-import: bad M line: {t}")));
461 }
462 let path = parts[2].as_bytes().to_vec();
463 if blob_ref == "inline" {
464 pending_inline = Some((mode, path));
465 continue;
466 }
467 let blob_oid = self.resolve_blob_ref(blob_ref)?;
468 ops.push(FileChangeOp::Modify {
469 mode,
470 blob_oid,
471 path,
472 });
473 continue;
474 }
475 if let Some(rest) = t.strip_prefix("D ") {
476 ops.push(FileChangeOp::Delete(rest.as_bytes().to_vec()));
477 continue;
478 }
479 if let Some(rest) = t.strip_prefix("N ") {
480 if !notes_ref {
481 return Err(Error::IndexError(format!(
482 "fast-import: N (notemodify) only allowed on refs/notes/*, not {refname}"
483 )));
484 }
485 let (data_ref, commit_spec) = parse_notemodify_operands(rest)?;
486 let target_commit = self.resolve_note_target_commit(commit_spec)?;
487 let blob_oid = match data_ref {
488 NoteBlobSpec::Inline => {
489 let next = self.read_line_nonempty()?.ok_or_else(|| {
490 Error::IndexError(
491 "fast-import: expected data after N inline".to_owned(),
492 )
493 })?;
494 let nt = next.trim_end();
495 if !nt.starts_with("data ") {
496 return Err(Error::IndexError(format!(
497 "fast-import: expected data after N inline, got: {nt}"
498 )));
499 }
500 let payload = self.read_data_payload(nt)?;
501 self.repo.odb.write(ObjectKind::Blob, &payload)?
502 }
503 NoteBlobSpec::Mark(id) => *self.marks.get(&id).ok_or_else(|| {
504 Error::IndexError(format!("fast-import: unknown mark :{id}"))
505 })?,
506 NoteBlobSpec::Oid(oid) => {
507 if oid.is_zero() {
508 ObjectId::zero()
509 } else {
510 let obj = self.repo.odb.read(&oid)?;
511 if obj.kind != ObjectKind::Blob {
512 return Err(Error::IndexError(format!(
513 "fast-import: N dataref {oid} is not a blob"
514 )));
515 }
516 oid
517 }
518 }
519 };
520 ops.push(FileChangeOp::NoteModify {
521 blob_oid,
522 target_commit,
523 });
524 continue;
525 }
526 if let Some(rest) = t.strip_prefix("R ") {
527 let parts: Vec<&str> = rest.split_whitespace().collect();
528 if parts.len() != 2 {
529 return Err(Error::IndexError(format!("fast-import: bad R line: {t}")));
530 }
531 ops.push(FileChangeOp::Rename(
532 parts[0].as_bytes().to_vec(),
533 parts[1].as_bytes().to_vec(),
534 ));
535 continue;
536 }
537 if let Some(rest) = t.strip_prefix("C ") {
538 let parts: Vec<&str> = rest.split_whitespace().collect();
539 if parts.len() != 2 {
540 return Err(Error::IndexError(format!("fast-import: bad C line: {t}")));
541 }
542 ops.push(FileChangeOp::Copy(
543 parts[0].as_bytes().to_vec(),
544 parts[1].as_bytes().to_vec(),
545 ));
546 continue;
547 }
548 self.stashed_line = Some(line);
549 break;
550 }
551
552 if pending_inline.is_some() {
553 return Err(Error::IndexError(
554 "fast-import: unterminated M ... inline (missing data)".to_owned(),
555 ));
556 }
557
558 let empty_tree: ObjectId = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
559 .parse()
560 .map_err(|_| Error::IndexError("fast-import: empty tree oid".to_owned()))?;
561
562 let mut parents: Vec<ObjectId> = Vec::new();
563 if let Some(oid) = from_oid {
564 parents.push(oid);
565 }
566 parents.extend(merge_oids);
567
568 let (parent_tree, parents_for_commit) = if let Some(&first_parent) = parents.first() {
569 let obj = self.repo.odb.read(&first_parent)?;
570 if obj.kind != ObjectKind::Commit {
571 return Err(Error::IndexError(format!(
572 "fast-import: parent {first_parent} is not a commit"
573 )));
574 }
575 let c = parse_commit(&obj.data)?;
576 (c.tree, parents)
577 } else if let Some(tip) = self.branch_tips.get(refname).copied() {
578 let obj = self.repo.odb.read(&tip)?;
579 if obj.kind != ObjectKind::Commit {
580 return Err(Error::IndexError(format!(
581 "fast-import: branch tip {tip} is not a commit"
582 )));
583 }
584 let c = parse_commit(&obj.data)?;
585 (c.tree, vec![tip])
586 } else {
587 (empty_tree, Vec::new())
588 };
589
590 let mut index = tree_to_index(&self.repo.odb, &parent_tree)?;
591 for op in ops {
592 match op {
593 FileChangeOp::DeleteAll => index.entries.clear(),
594 FileChangeOp::Delete(path) => {
595 index.entries.retain(|e| e.path != path);
596 }
597 FileChangeOp::Modify {
598 mode,
599 blob_oid,
600 path,
601 } => {
602 let mode = normalize_mode(mode)?;
603 index.add_or_replace(index_entry(path, mode, blob_oid));
604 }
605 FileChangeOp::NoteModify {
606 blob_oid,
607 target_commit,
608 } => {
609 remove_note_entries_for_target(&mut index, &target_commit);
610 if !blob_oid.is_zero() {
611 let after_remove = count_notes_in_index(&index);
612 let fanout = notes_fanout_for_count(after_remove.saturating_add(1));
613 let note_path = construct_note_path_with_fanout(&target_commit, fanout);
614 index.add_or_replace(index_entry(note_path, MODE_REGULAR, blob_oid));
615 }
616 }
617 FileChangeOp::Rename(src, dst) => {
618 let Some(pos) = index.entries.iter().position(|e| e.path == src) else {
619 return Err(Error::IndexError(format!(
620 "fast-import: filerename source missing: {}",
621 String::from_utf8_lossy(&src)
622 )));
623 };
624 let mut ent = index.entries.remove(pos);
625 ent.path = dst;
626 index.add_or_replace(ent);
627 }
628 FileChangeOp::Copy(src, dst) => {
629 let Some(ent) = index.entries.iter().find(|e| e.path == src).cloned() else {
630 return Err(Error::IndexError(format!(
631 "fast-import: filecopy source missing: {}",
632 String::from_utf8_lossy(&src)
633 )));
634 };
635 let mut copy_ent = ent;
636 copy_ent.path = dst;
637 index.add_or_replace(copy_ent);
638 }
639 }
640 }
641
642 if notes_ref && count_notes_in_index(&index) > 0 {
643 let n = count_notes_in_index(&index);
644 rewrite_notes_fanout_in_index(&self.repo.odb, &mut index, notes_fanout_for_count(n))?;
645 }
646
647 let tree_oid = write_tree_from_index(&self.repo.odb, &index, "")?;
648
649 let message_str = String::from_utf8_lossy(&message).into_owned();
650 let raw_message = (!message.is_empty() && std::str::from_utf8(&message).is_err())
651 .then_some(message.clone());
652 let reflog_identity = committer.clone();
653
654 let commit = CommitData {
655 tree: tree_oid,
656 parents: parents_for_commit,
657 author,
658 committer,
659 author_raw: Vec::new(),
660 committer_raw: Vec::new(),
661 encoding: None,
662 message: message_str,
663 raw_message,
664 };
665 let bytes = serialize_commit(&commit);
666 let commit_oid = self.repo.odb.write(ObjectKind::Commit, &bytes)?;
667
668 if let Some(m) = mark {
669 self.marks.insert(m, commit_oid);
670 }
671 self.branch_tips.insert(refname.to_string(), commit_oid);
672 if !self.force {
673 if let Ok(old) = crate::refs::resolve_ref(&self.repo.git_dir, refname) {
674 if old != commit_oid {
675 let is_ancestor =
676 crate::merge_base::is_ancestor(self.repo, old, commit_oid).unwrap_or(false);
677 if !is_ancestor {
678 return Err(Error::IndexError(format!(
679 "fast-import: refusing non-fast-forward update of {refname} (use feature force or --force)"
680 )));
681 }
682 }
683 }
684 }
685 self.update_ref_for_fast_import(refname, &commit_oid, &reflog_identity, "fast-import")?;
686 Ok(())
687 }
688
689 fn resolve_commit_ish(&self, spec: &str) -> Result<ObjectId> {
690 if let Some(rest) = spec.strip_prefix(':') {
691 let id: u32 = rest
692 .parse()
693 .map_err(|_| Error::IndexError(format!("fast-import: bad mark ref: {spec}")))?;
694 return self
695 .marks
696 .get(&id)
697 .copied()
698 .ok_or_else(|| Error::IndexError(format!("fast-import: unknown mark :{id}")));
699 }
700 if spec.len() == 40 && spec.chars().all(|c| c.is_ascii_hexdigit()) {
701 return spec.parse();
702 }
703 resolve_revision(self.repo, spec)
704 }
705
706 fn resolve_blob_ref(&self, spec: &str) -> Result<ObjectId> {
707 if let Some(rest) = spec.strip_prefix(':') {
708 let id: u32 = rest
709 .parse()
710 .map_err(|_| Error::IndexError(format!("fast-import: bad mark ref: {spec}")))?;
711 return self
712 .marks
713 .get(&id)
714 .copied()
715 .ok_or_else(|| Error::IndexError(format!("fast-import: unknown mark :{id}")));
716 }
717 if spec.len() == 40 && spec.chars().all(|c| c.is_ascii_hexdigit()) {
718 return spec.parse();
719 }
720 Err(Error::IndexError(format!(
721 "fast-import: unsupported blob ref: {spec}"
722 )))
723 }
724
725 fn read_tag(&mut self, short_name: &str) -> Result<()> {
726 let mut mark: Option<u32> = None;
727 let mut from_oid: Option<ObjectId> = None;
728 let mut tagger: Option<String> = None;
729
730 loop {
731 let line = self.read_line_nonempty()?.ok_or_else(|| {
732 Error::IndexError("fast-import: unexpected EOF in tag".to_owned())
733 })?;
734 let t = line.trim_end();
735 if let Some(id) = t.strip_prefix("mark :") {
736 mark = Some(
737 id.parse()
738 .map_err(|_| Error::IndexError(format!("fast-import: bad mark: {t}")))?,
739 );
740 continue;
741 }
742 if t.starts_with("original-oid ") {
743 continue;
744 }
745 if let Some(rest) = t.strip_prefix("from ") {
746 let spec = rest.trim();
747 from_oid = Some(self.resolve_commit_ish(spec)?);
748 continue;
749 }
750 if let Some(rest) = t.strip_prefix("tagger ") {
751 tagger = Some(rest.to_owned());
752 continue;
753 }
754 if t.starts_with("data ") {
755 let message = self.read_data_payload(t)?;
756
757 let target = from_oid
758 .ok_or_else(|| Error::IndexError("fast-import: tag missing from".to_owned()))?;
759 let target_obj = self.repo.odb.read(&target)?;
760 let object_type = target_obj.kind.as_str().to_owned();
761 let msg_str = String::from_utf8_lossy(&message).into_owned();
762
763 let reflog_ident = tagger
764 .clone()
765 .unwrap_or_else(Self::fast_import_reflog_identity_from_env);
766 let tag_data = TagData {
767 object: target,
768 object_type,
769 tag: short_name.to_owned(),
770 tagger,
771 message: msg_str,
772 };
773 let bytes = serialize_tag(&tag_data);
774 let tag_oid = self.repo.odb.write(ObjectKind::Tag, &bytes)?;
775
776 if let Some(m) = mark {
777 self.marks.insert(m, tag_oid);
778 }
779
780 let full_ref = format!("refs/tags/{short_name}");
781 self.update_ref_with_reflog(&full_ref, &tag_oid, &reflog_ident, "fast-import")?;
782 return Ok(());
783 }
784 return Err(Error::IndexError(format!(
785 "fast-import: unexpected in tag: {t}"
786 )));
787 }
788 }
789
790 fn read_reset(&mut self, refname: &str) -> Result<()> {
791 let Some(line) = self.read_line_any()? else {
792 return Ok(());
793 };
794 let t = line.trim_end();
795 if t.is_empty() {
796 return Ok(());
797 }
798 if let Some(spec) = t.strip_prefix("from ") {
799 let oid = self.resolve_commit_ish(spec.trim())?;
800 self.branch_tips.insert(refname.to_string(), oid);
801 if !self.force {
802 if let Ok(old) = crate::refs::resolve_ref(&self.repo.git_dir, refname) {
803 if old != oid {
804 let is_ancestor =
805 crate::merge_base::is_ancestor(self.repo, old, oid).unwrap_or(false);
806 if !is_ancestor {
807 return Err(Error::IndexError(format!(
808 "fast-import: refusing non-fast-forward reset of {refname}"
809 )));
810 }
811 }
812 }
813 }
814 let ident = Self::fast_import_reflog_identity_from_env();
815 self.update_ref_for_fast_import(refname, &oid, &ident, "fast-import")?;
816 return Ok(());
817 }
818 self.stashed_line = Some(line);
819 Ok(())
820 }
821
822 fn resolve_note_target_commit(&self, spec: &str) -> Result<ObjectId> {
824 let oid = if let Some(tip) = self.branch_tips.get(spec) {
825 *tip
826 } else {
827 self.resolve_commit_ish(spec)?
828 };
829 let obj = self.repo.odb.read(&oid)?;
830 if obj.kind != ObjectKind::Commit {
831 return Err(Error::IndexError(format!(
832 "fast-import: notemodify target {spec} is not a commit"
833 )));
834 }
835 Ok(oid)
836 }
837}
838
839fn validate_fast_import_refname(refname: &str) -> Result<()> {
840 if refname == "HEAD" {
841 return Ok(());
842 }
843 let full = if refname.starts_with("refs/") {
844 refname.to_owned()
845 } else {
846 format!("refs/heads/{refname}")
847 };
848 check_refname_format(&full, &RefNameOptions::default())
849 .map(|_| ())
850 .map_err(|e| Error::IndexError(format!("fast-import: invalid ref name '{refname}': {e}")))
851}
852
853enum NoteBlobSpec {
855 Inline,
856 Mark(u32),
857 Oid(ObjectId),
858}
859
860fn parse_notemodify_operands(rest: &str) -> Result<(NoteBlobSpec, &str)> {
861 let s = rest.trim();
862 if let Some(commit_spec) = s.strip_prefix("inline ") {
863 return Ok((NoteBlobSpec::Inline, commit_spec.trim()));
864 }
865 if let Some(after_colon) = s.strip_prefix(':') {
866 let space = after_colon
867 .find(' ')
868 .ok_or_else(|| Error::IndexError("fast-import: bad N line (mark)".to_owned()))?;
869 let id: u32 = after_colon[..space]
870 .parse()
871 .map_err(|_| Error::IndexError(format!("fast-import: bad mark in N line: {s}")))?;
872 return Ok((NoteBlobSpec::Mark(id), after_colon[space + 1..].trim()));
873 }
874 if s.len() < 41 {
875 return Err(Error::IndexError(format!(
876 "fast-import: bad N line (expected oid + commit-ish): {s}"
877 )));
878 }
879 let head = &s[..40];
880 if !head.chars().all(|c| c.is_ascii_hexdigit()) {
881 return Err(Error::IndexError(format!(
882 "fast-import: bad N line (invalid blob oid): {s}"
883 )));
884 }
885 if s.as_bytes().get(40) != Some(&b' ') {
886 return Err(Error::IndexError(format!(
887 "fast-import: bad N line (missing space after blob oid): {s}"
888 )));
889 }
890 let oid: ObjectId = head
891 .parse()
892 .map_err(|_| Error::IndexError(format!("fast-import: bad blob oid in N line: {s}")))?;
893 Ok((NoteBlobSpec::Oid(oid), s[41..].trim()))
894}
895
896fn is_note_index_path(path: &[u8]) -> bool {
897 let compact: Vec<u8> = path.iter().copied().filter(|b| *b != b'/').collect();
898 compact.len() == 40 && compact.iter().all(u8::is_ascii_hexdigit)
899}
900
901fn compact_hex_from_note_path(path: &[u8]) -> Option<String> {
902 if !is_note_index_path(path) {
903 return None;
904 }
905 let s: String = path
906 .iter()
907 .copied()
908 .filter(|b| *b != b'/')
909 .map(|b| char::from(b).to_ascii_lowercase())
910 .collect();
911 Some(s)
912}
913
914fn count_notes_in_index(index: &crate::index::Index) -> usize {
915 index
916 .entries
917 .iter()
918 .filter(|e| is_note_index_path(&e.path))
919 .count()
920}
921
922fn notes_fanout_for_count(mut n: usize) -> usize {
923 let mut fanout = 0usize;
924 while n > 0xff {
925 n >>= 8;
926 fanout += 1;
927 }
928 fanout
929}
930
931fn construct_note_path_with_fanout(commit: &ObjectId, fanout: usize) -> Vec<u8> {
932 let hex = commit.to_hex();
933 let bytes = hex.as_bytes();
934 let split = fanout.min(bytes.len() / 2);
935 let mut out = Vec::with_capacity(hex.len() + split);
936 for i in 0..split {
937 let start = i * 2;
938 out.extend_from_slice(&bytes[start..start + 2]);
939 out.push(b'/');
940 }
941 out.extend_from_slice(&bytes[split * 2..]);
942 out
943}
944
945fn remove_note_entries_for_target(index: &mut crate::index::Index, target: &ObjectId) {
946 let want = target.to_hex();
947 index.entries.retain(|e| {
948 if !is_note_index_path(&e.path) {
949 return true;
950 }
951 compact_hex_from_note_path(&e.path).as_deref() != Some(want.as_str())
952 });
953}
954
955fn rewrite_notes_fanout_in_index(
956 odb: &crate::odb::Odb,
957 index: &mut crate::index::Index,
958 fanout: usize,
959) -> Result<()> {
960 let mut notes: Vec<(ObjectId, ObjectId, u32)> = Vec::new();
961 let mut kept = Vec::new();
962 for e in index.entries.drain(..) {
963 if is_note_index_path(&e.path) {
964 let Some(compact) = compact_hex_from_note_path(&e.path) else {
965 continue;
966 };
967 let commit_oid = compact
968 .parse()
969 .map_err(|_| Error::IndexError("fast-import: bad note path in index".to_owned()))?;
970 notes.push((commit_oid, e.oid, e.mode));
971 } else {
972 kept.push(e);
973 }
974 }
975 index.entries = kept;
976 let mut by_commit: std::collections::BTreeMap<ObjectId, (ObjectId, u32)> =
977 std::collections::BTreeMap::new();
978 for (commit_oid, blob_oid, mode) in notes {
979 if let Some((existing_oid, existing_mode)) = by_commit.get_mut(&commit_oid) {
980 if *existing_oid != blob_oid {
981 let existing = odb.read(existing_oid)?;
982 let incoming = odb.read(&blob_oid)?;
983 let mut existing_len = existing.data.len();
984 if existing_len > 0 && existing.data[existing_len - 1] == b'\n' {
985 existing_len -= 1;
986 }
987 let mut data = Vec::with_capacity(existing_len + 2 + incoming.data.len());
988 data.extend_from_slice(&existing.data[..existing_len]);
989 data.push(b'\n');
990 data.push(b'\n');
991 data.extend_from_slice(&incoming.data);
992 *existing_oid = odb.write(ObjectKind::Blob, &data)?;
993 }
994 *existing_mode = mode;
995 } else {
996 by_commit.insert(commit_oid, (blob_oid, mode));
997 }
998 }
999 for (commit_oid, (blob_oid, mode)) in by_commit {
1000 let path = construct_note_path_with_fanout(&commit_oid, fanout);
1001 index.add_or_replace(index_entry(path, mode, blob_oid));
1002 }
1003 Ok(())
1004}
1005
1006fn normalize_mode(mode: u32) -> Result<u32> {
1007 match mode {
1008 0o100644 | 0o644 => Ok(MODE_REGULAR),
1009 0o100755 | 0o755 => Ok(crate::index::MODE_EXECUTABLE),
1010 0o120000 => Ok(crate::index::MODE_SYMLINK),
1011 0o160000 => Ok(MODE_GITLINK),
1012 0o040000 => Ok(MODE_TREE),
1013 _ => Err(Error::IndexError(format!(
1014 "fast-import: unsupported mode {mode:o}"
1015 ))),
1016 }
1017}
1018
1019fn index_entry(path: Vec<u8>, mode: u32, oid: ObjectId) -> IndexEntry {
1020 let path_len = path.len().min(0xFFF) as u16;
1021 IndexEntry {
1022 ctime_sec: 0,
1023 ctime_nsec: 0,
1024 mtime_sec: 0,
1025 mtime_nsec: 0,
1026 dev: 0,
1027 ino: 0,
1028 mode,
1029 uid: 0,
1030 gid: 0,
1031 size: 0,
1032 oid,
1033 flags: path_len,
1034 flags_extended: Some(0),
1035 path,
1036 base_index_pos: 0,
1037 }
1038}
1039
1040fn tree_to_index(odb: &crate::odb::Odb, tree_oid: &ObjectId) -> Result<Index> {
1041 let obj = odb.read(tree_oid)?;
1042 if obj.kind != ObjectKind::Tree {
1043 return Err(Error::IndexError(format!("expected tree at {tree_oid}")));
1044 }
1045 let entries = crate::objects::parse_tree(&obj.data)?;
1046 let mut index = Index::new();
1047 for te in entries {
1048 let path = te.name;
1049 if te.mode == MODE_TREE {
1050 let sub = tree_to_index(odb, &te.oid)?;
1051 for mut e in sub.entries {
1052 let mut full = path.clone();
1053 full.push(b'/');
1054 full.extend_from_slice(&e.path);
1055 e.path = full;
1056 let pl = e.path.len().min(0xFFF) as u16;
1057 e.flags = pl;
1058 index.add_or_replace(e);
1059 }
1060 } else {
1061 index.add_or_replace(index_entry(path, te.mode, te.oid));
1062 }
1063 }
1064 Ok(index)
1065}
1066
1067#[cfg(test)]
1068mod tests {
1069 use super::*;
1070 use crate::refs::resolve_ref;
1071 use crate::repo::init_repository;
1072 use std::io::Cursor;
1073 use tempfile::tempdir;
1074
1075 #[test]
1076 fn fast_import_delimited_data_m_inline_and_note() -> Result<()> {
1077 let dir =
1078 tempdir().map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
1079 let repo = init_repository(dir.path(), false, "main", None, "files")?;
1080
1081 let setup = r#"commit refs/heads/main
1082committer T <t@e> 1000000000 +0000
1083data <<COMMIT
1084m1
1085COMMIT
1086
1087M 644 inline f
1088data <<EOF
1089a
1090EOF
1091
1092commit refs/heads/main
1093committer T <t@e> 1000000001 +0000
1094data <<COMMIT
1095m2
1096COMMIT
1097
1098M 644 inline f
1099data <<EOF
1100b
1101EOF
1102
1103"#;
1104 import_stream(&repo, Cursor::new(setup.as_bytes()))?;
1105
1106 let c2 = resolve_ref(&repo.git_dir, "refs/heads/main")?;
1107 let c2_obj = repo.odb.read(&c2)?;
1108 let c2_parsed = parse_commit(&c2_obj.data)?;
1109 let c1 = c2_parsed
1110 .parents
1111 .first()
1112 .copied()
1113 .ok_or_else(|| Error::IndexError("test: expected parent commit".to_owned()))?;
1114
1115 let notes = format!(
1116 r#"commit refs/notes/commits
1117committer T <t@e> 1000000002 +0000
1118data <<COMMIT
1119n1
1120COMMIT
1121
1122N inline {c1}
1123data <<EOF
1124note1
1125EOF
1126
1127N inline {c2}
1128data <<EOF
1129note2
1130EOF
1131
1132commit refs/notes/commits
1133committer T <t@e> 1000000003 +0000
1134data <<COMMIT
1135n2
1136COMMIT
1137
1138M 644 inline foobar/x.txt
1139data <<EOF
1140non-note
1141EOF
1142
1143N inline {c2}
1144data <<EOF
1145edited
1146EOF
1147
1148"#
1149 );
1150 import_stream(&repo, Cursor::new(notes.as_bytes()))?;
1151
1152 let notes_tip = resolve_ref(&repo.git_dir, "refs/notes/commits")?;
1153 let commit_obj = repo.odb.read(¬es_tip)?;
1154 let parsed = parse_commit(&commit_obj.data)?;
1155 let tree = tree_to_index(&repo.odb, &parsed.tree)?;
1156 assert!(
1157 tree.entries.iter().any(|e| e.path == b"foobar/x.txt"),
1158 "expected non-note path preserved"
1159 );
1160 let mut found_edit = false;
1161 for e in &tree.entries {
1162 if is_note_index_path(&e.path) {
1163 let compact = compact_hex_from_note_path(&e.path).expect("note path");
1164 if compact == c2.to_hex() {
1165 let blob = repo.odb.read(&e.oid)?;
1166 assert_eq!(blob.data, b"edited\n");
1167 found_edit = true;
1168 }
1169 }
1170 }
1171 assert!(found_edit, "expected edited note for second commit");
1172 Ok(())
1173 }
1174}