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