Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/src/mirror.rs

39.2 KiB, 79 runs

created by r2848102244:120, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Keeping a git mirror of the history current.
2//!
3//! A repository can live in Ore while a git forge still serves its readers.
4//! That is what the mirror is for, and it is also what makes the move
5//! reversible: the door out is the door in reversed. It is not a verb. The
6//! configuration names a directory and every verb that appends operations
7//! brings the mirror current when it finishes, so there is nothing to remember
8//! and no moment at which the mirror is behind. Capture works the same way, and
9//! for the same reason.
10//!
11//! # A mark is a commit
12//!
13//! Ore has operations, causality and marks, and no commits. An
14//! [`Op::Mark`] names a point in history so that it can be referred to later,
15//! which is what a git commit is for, and the import already reads a commit as
16//! exactly that. So each mark becomes a commit whose tree is the render of that
17//! mark's ancestry -- the same tree `ore back` puts in the working copy -- and
18//! whose parents are the marks nearest to it in the operation graph. Two lines
19//! of work joined by a later mark therefore become a git merge commit: the mark
20//! graph and the commit graph are the same graph.
21//!
22//! A mark's ancestry is causally closed the moment the mark is written and no
23//! later operation can join it, so a mark's tree and a mark's parents never
24//! change. **The mirror is append-only because a mark's commit is a function of
25//! a fixed causal past**, not because anything here is careful.
26//!
27//! # And the tail is a commit too
28//!
29//! Between the last mark and now there is usually work, because every verb
30//! captures. One further commit carries it: its tree is the render of the whole
31//! log, which is what the working copy holds, and its parents are the heads of
32//! the mark graph together with wherever the branch already stood. It is the
33//! only object invented here, it is the only one that is not a function of the
34//! history alone, and parenting it on the branch's previous state is what keeps
35//! the branch fast-forwarding for anybody who has cloned it.
36//!
37//! Where the working copy stands exactly at one mark, no such commit is made and
38//! the branch points at that mark's own.
39//!
40//! # What is written and what is lost
41//!
42//! Git's author and committer fields are self-declared strings and prove
43//! nothing, which is why Ore signs operations instead. They are written anyway,
44//! because a mirror nobody can read is no mirror. The committer is the replica
45//! whose mark the commit is, spelled with an identity line from the map where
46//! there is one and with the replica's own number where there is not. The author
47//! is the line the commit was authored under before it was ever imported, which
48//! a mark carries in its body as an `Ore-Author` trailer, whose spelling is
49//! [`oxedyne_fe2o3_ore::op::AUTHOR_TRAILER`] because a mirror, an importer and a
50//! forge all have to read it the same way. The value is git's own author line,
51//! `Name <email> 1735089438 +0800` -- identity, moment and zone offset at once.
52//!
53//! That trailer is there because the map runs the wrong way for this question.
54//! One person who wrote under three identity lines is **one** replica inside
55//! Ore, which is the truth about who signed the work; reversing the map to write
56//! the mirror then finds three keys on one value and can only pick one of them,
57//! so a history's second and third names vanish on the way out. The trailer
58//! keeps both sides faithful: one signer here, and every commit under the name
59//! it was written with there.
60//!
61//! A commit carrying several replicas' operations can name only one of them, and
62//! the signatures cannot cross at all. What the mirror loses, the log keeps, and
63//! the `Ore-Mark` line in every message says where to look.
64//!
65//! # Dates
66//!
67//! Nothing in the engine reads a clock, and nothing here decides anything by a
68//! date. A mark may carry one all the same -- the moment `ore mark` was run, or
69//! the author date of the git commit an import read -- and where it does, that
70//! is the commit's date, so an imported history mirrors back out over the years
71//! it was actually written in. Where a mark carries none, the mirror remembers
72//! when it first saw it and reuses that for ever. Either way a commit, once
73//! written, never changes.
74//!
75//! A mark holds seconds in UTC and no zone, so the offset an author read off
76//! their own clock rides in the trailer with the identity, git's own author line
77//! being all three at once. **Only the offset is taken from it.** The instant
78//! comes from the mark, which is inside the signature; taking it from the
79//! trailer would let something nobody signed move a commit's date. Where nothing
80//! recorded an offset -- a mark made in Ore, a lightweight tag's mark -- the
81//! commit is written at `+0000`, which says the instant is known and the wall
82//! clock is not.
83//!
84//! # Refuse rather than mirror something wrong
85//!
86//! Git names a path either a file or a directory and never both, and handed both
87//! it keeps the directory and drops the file without a word. The engine refuses
88//! such a tree by name, along with a path holding `.git` and the other shapes no
89//! git tree can hold. A refusal is reported and does not fail the verb: the
90//! history is the thing that matters, and a mirror that cannot be written must
91//! not stop somebody recording an edit.
92
93use crate::repo::{
94 Config,
95 Repo,
96};
97use crate::tree;
98
99use oxedyne_fe2o3_core::prelude::*;
100use oxedyne_fe2o3_jdat::prelude::*;
101use oxedyne_fe2o3_ore::fastexport::{
102 split_identity_line,
103 FileMode,
104 ObjRef,
105 Person,
106 TzOffset,
107 When,
108};
109use oxedyne_fe2o3_ore::gitexport::{
110 self,
111 Commit,
112 Entry,
113 Stream,
114};
115use oxedyne_fe2o3_ore::id::OpId;
116use oxedyne_fe2o3_ore::op::{
117 without_author,
118 Mode,
119 Op,
120};
121
122use std::collections::{
123 BTreeMap,
124 BTreeSet,
125};
126use std::fs;
127use std::io::Write;
128use std::path::{
129 Path,
130 PathBuf,
131};
132use std::process::{
133 Command,
134 Stdio,
135};
136use std::time::{
137 SystemTime,
138 UNIX_EPOCH,
139};
140
141
142/// What the mirror's own bookkeeping is called, inside `.ore`.
143pub const STATE_FILE: &str = "mirror";
144
145/// Version of that bookkeeping's format.
146pub const STATE_VERSION: u64 = 1;
147
148/// The branch a reader clones.
149pub const BRANCH: &str = "main";
150
151// No zone
152// What a commit is written at when nothing recorded the offset its author read:
153// a mark made in Ore holds seconds in UTC and the tool reads no zone at all, and
154// a lightweight tag's mark holds not even that. `+0000` says the instant is
155// known and the wall clock is not, which is exactly the case; `-0000` is git's
156// spelling for a zone that was recorded and is unknown, and is a different
157// claim.
158const UNZONED: TzOffset = TzOffset { mins: 0, neg: false };
159
160// Tag length
161// Git keeps a reference in a file named after it, and a file name is 255 bytes
162// on every filesystem this runs on. A mark is named after a commit's subject and
163// git's subject rule folds a wrapped one onto a single line, which on fe2o3's
164// history comes to 312 characters at the longest. What is left over is room for
165// the `-r<replica>-<counter>` a shared tag takes.
166const TAG_LIMIT: usize = 200;
167
168/// The reference every commit is written to while the stream is being applied,
169/// removed once the branch and the tags name what matters.
170pub const STAGING: &str = "refs/ore/staging";
171
172/// Where git writes back the object name of every mark the stream minted.
173const MARK_FILE: &str = "ore-marks";
174
175/// The domain a replica with no known identity gets an address in, reserved by
176/// RFC 2606 precisely so that it can never be somebody's real mailbox.
177const NO_DOMAIN: &str = "replica.invalid";
178
179
180// ---------------------------------------------------------------------------
181// What the mirror remembers.
182// ---------------------------------------------------------------------------
183
184/// One mark that has been written into the mirror.
185#[derive(Clone, Debug)]
186pub struct Exported {
187 pub mark: OpId, // the mark operation
188 // What the commit was dated: the mark's own time where it carries one, and
189 // otherwise the moment the mirror first saw the mark. A commit that changed
190 // its date would be a different commit, so whichever it was is kept for good.
191 pub secs: i64,
192 pub tag: String, // the tag pointing at it, without `refs/tags/`
193 pub commit: String, // the object name of its commit
194}
195
196impl Exported {
197 /// Serialises the record to a [`Dat`].
198 pub fn to_dat(&self) -> Dat {
199 Dat::List(vec![
200 Dat::U64(self.mark.replica.inner()),
201 Dat::U64(self.mark.counter),
202 Dat::I64(self.secs),
203 Dat::Str(self.tag.clone()),
204 Dat::Str(self.commit.clone()),
205 ])
206 }
207
208 /// Reconstructs the record from a [`Dat`].
209 pub fn from_dat(dat: &Dat)
210 -> Outcome<Self>
211 {
212 let v = match dat {
213 Dat::List(l) if l.len() == 5 => l,
214 other => return Err(err!(
215 "An exported mark expects a list of five, got {:?}.", other;
216 Decode, Input, Mismatch)),
217 };
218 Ok(Self {
219 mark: OpId::new(
220 oxedyne_fe2o3_ore::id::ReplicaId::new(res!(number(&v[0]))),
221 res!(number(&v[1])),
222 ),
223 secs: res!(number(&v[2])) as i64,
224 tag: res!(text(&v[3])),
225 commit: res!(text(&v[4])),
226 })
227 }
228}
229
230
231/// What the mirror knows about itself between runs.
232#[derive(Clone, Debug, Default)]
233pub struct State {
234 /// Every mark written, in ascending order of identity.
235 pub marks: Vec<Exported>,
236 /// The commit the branch was last set to.
237 pub head: Option<String>,
238 /// The mark that commit stands for, where the branch points straight at one
239 /// rather than at a commit made for the tail.
240 pub head_mark: Option<OpId>,
241 /// The log's frontier when the branch was last set, which is what says
242 /// whether there is anything to do.
243 pub frontier: Vec<OpId>,
244}
245
246impl State {
247 /// Serialises the bookkeeping to a [`Dat`].
248 pub fn to_dat(&self) -> Dat {
249 let mut map = DaticleMap::new();
250 map.insert(Dat::Str(fmt!("format")), Dat::U64(STATE_VERSION));
251 map.insert(Dat::Str(fmt!("marks")), Dat::List(
252 self.marks.iter().map(|m| m.to_dat()).collect(),
253 ));
254 if let Some(head) = &self.head {
255 map.insert(Dat::Str(fmt!("head")), Dat::Str(head.clone()));
256 }
257 if let Some(id) = &self.head_mark {
258 map.insert(Dat::Str(fmt!("head_mark")), Dat::List(vec![
259 Dat::U64(id.replica.inner()),
260 Dat::U64(id.counter),
261 ]));
262 }
263 map.insert(Dat::Str(fmt!("frontier")), Dat::List(
264 self.frontier.iter().map(|id| Dat::List(vec![
265 Dat::U64(id.replica.inner()),
266 Dat::U64(id.counter),
267 ])).collect(),
268 ));
269 Dat::Map(map)
270 }
271
272 /// Reconstructs the bookkeeping from a [`Dat`].
273 pub fn from_dat(dat: &Dat)
274 -> Outcome<Self>
275 {
276 let map = match dat {
277 Dat::Map(m) => m,
278 other => return Err(err!(
279 "The mirror bookkeeping expects a map, got {:?}.", other;
280 Decode, Input, Mismatch)),
281 };
282 let format = match map.get(&Dat::Str(fmt!("format"))) {
283 Some(dat) => res!(number(dat)),
284 None => return Err(err!(
285 "The mirror bookkeeping has no field \"format\".";
286 Decode, Input, Missing)),
287 };
288 if format != STATE_VERSION {
289 return Err(err!(
290 "The mirror bookkeeping declares format version {}, and this tool \
291 knows only version {}.", format, STATE_VERSION;
292 Decode, Input, Version, Mismatch));
293 }
294 let mut marks = Vec::new();
295 if let Some(Dat::List(l)) = map.get(&Dat::Str(fmt!("marks"))) {
296 for item in l {
297 marks.push(res!(Exported::from_dat(item)));
298 }
299 }
300 let head = match map.get(&Dat::Str(fmt!("head"))) {
301 Some(dat) => Some(res!(text(dat))),
302 None => None,
303 };
304 let head_mark = match map.get(&Dat::Str(fmt!("head_mark"))) {
305 Some(dat) => Some(res!(op_id(dat))),
306 None => None,
307 };
308 let mut frontier = Vec::new();
309 if let Some(Dat::List(l)) = map.get(&Dat::Str(fmt!("frontier"))) {
310 for item in l {
311 frontier.push(res!(op_id(item)));
312 }
313 }
314 Ok(Self { marks, head, head_mark, frontier })
315 }
316}
317
318/// Reads a number out of a [`Dat`], whatever width it was written at.
319fn number(dat: &Dat)
320 -> Outcome<u64>
321{
322 match dat {
323 Dat::U64(n) => Ok(*n),
324 Dat::U32(n) => Ok(*n as u64),
325 Dat::U16(n) => Ok(*n as u64),
326 Dat::U8(n) => Ok(*n as u64),
327 Dat::I64(n) => Ok(*n as u64),
328 Dat::I32(n) => Ok(*n as u64),
329 other => Err(err!(
330 "The mirror bookkeeping expects a number, got {:?}.", other;
331 Decode, Input, Mismatch)),
332 }
333}
334
335/// Reads a string out of a [`Dat`].
336fn text(dat: &Dat)
337 -> Outcome<String>
338{
339 match dat {
340 Dat::Str(s) => Ok(s.clone()),
341 other => Err(err!(
342 "The mirror bookkeeping expects a string, got {:?}.", other;
343 Decode, Input, Mismatch)),
344 }
345}
346
347/// Reads an operation identifier out of a [`Dat`].
348fn op_id(dat: &Dat)
349 -> Outcome<OpId>
350{
351 match dat {
352 Dat::List(l) if l.len() == 2 => Ok(OpId::new(
353 oxedyne_fe2o3_ore::id::ReplicaId::new(res!(number(&l[0]))),
354 res!(number(&l[1])),
355 )),
356 other => Err(err!(
357 "The mirror bookkeeping expects an operation identifier as a pair, got \
358 {:?}.", other;
359 Decode, Input, Mismatch)),
360 }
361}
362
363
364// ---------------------------------------------------------------------------
365// What one run did.
366// ---------------------------------------------------------------------------
367
368/// What bringing the mirror current amounted to.
369#[derive(Clone, Debug, Default)]
370pub struct Report {
371 /// Where the mirror is.
372 pub path: PathBuf,
373 /// How many mark commits were written.
374 pub commits: usize,
375 /// How many tags were pointed somewhere.
376 pub tags: usize,
377 /// Whether the branch moved.
378 pub moved: bool,
379 /// Whether a commit was made for work that no mark names yet.
380 pub tail: bool,
381}
382
383impl Report {
384 /// Reports whether the run had nothing to do.
385 pub fn is_empty(&self) -> bool {
386 self.commits == 0 && self.tags == 0 && !self.moved
387 }
388}
389
390
391// ---------------------------------------------------------------------------
392// Bringing it current.
393// ---------------------------------------------------------------------------
394
395/// Brings the configured mirror up to date with the log, if one is configured.
396///
397/// `Ok(None)` is a repository that keeps no mirror, which is most of them.
398pub fn update(repo: &Repo)
399 -> Outcome<Option<Report>>
400{
401 let named = match &repo.cfg.mirror {
402 Some(path) => path.clone(),
403 None => return Ok(None),
404 };
405 let path = {
406 let given = PathBuf::from(&named);
407 if given.is_absolute() { given } else { repo.root.join(given) }
408 };
409 if repo.log.is_empty() {
410 return Ok(Some(Report { path, ..Report::default() }));
411 }
412 res!(ensure_repo(&path));
413 let mut state = res!(read_state(&repo.dir));
414 let report = res!(write_out(repo, &path, &mut state));
415 if !report.is_empty() {
416 res!(save_state(&repo.dir, &state));
417 }
418 Ok(Some(report))
419}
420
421/// Returns the path of the mirror's bookkeeping.
422pub fn state_path(dir: &Path) -> PathBuf {
423 dir.join(STATE_FILE)
424}
425
426/// Is the mirror already at this frontier, so that a command appending nothing
427/// has nothing to bring it?
428///
429/// Asked by a command that means to answer without opening the log: a mirror
430/// behind the history is work, and work needs the render. A repository naming no
431/// mirror has nothing to be behind.
432pub fn settled(dir: &Path, cfg: &Config, frontier: &[OpId])
433 -> Outcome<bool>
434{
435 if cfg.mirror.is_none() {
436 return Ok(true);
437 }
438 let state = res!(read_state(dir));
439 Ok(state.frontier == frontier && state.head.is_some())
440}
441
442/// Reads the mirror's bookkeeping, which a repository need not have.
443pub fn read_state(dir: &Path)
444 -> Outcome<State>
445{
446 let path = state_path(dir);
447 if !path.is_file() {
448 return Ok(State::default());
449 }
450 let text = match fs::read_to_string(&path) {
451 Ok(t) => t,
452 Err(e) => return Err(err!(e,
453 "The mirror bookkeeping {:?} could not be read.", path;
454 IO, File, Read)),
455 };
456 let dat = match Dat::decode_string(text) {
457 Ok(d) => d,
458 Err(e) => return Err(err!(e,
459 "The mirror bookkeeping {:?} is not readable JDAT.", path;
460 Decode, Input)),
461 };
462 State::from_dat(&dat)
463}
464
465/// Writes the mirror's bookkeeping.
466pub fn save_state(dir: &Path, state: &State)
467 -> Outcome<()>
468{
469 let text = res!(state.to_dat().jdat_to_lines(" "));
470 let path = state_path(dir);
471 match fs::write(&path, fmt!("{}\n", text)) {
472 Ok(()) => Ok(()),
473 Err(e) => Err(err!(e,
474 "The mirror bookkeeping {:?} could not be written.", path;
475 IO, File, Write)),
476 }
477}
478
479
480/// Makes sure the mirror is a bare git repository, making one if it is not.
481///
482/// Bare, because a mirror has no working copy of its own to fall stale: a reader
483/// clones it, and pushing it to a forge is git's business and not this tool's.
484fn ensure_repo(path: &Path)
485 -> Outcome<()>
486{
487 if path.join("HEAD").is_file() && path.join("objects").is_dir() {
488 return Ok(());
489 }
490 if path.exists() && !path.is_dir() {
491 return Err(err!(
492 "The mirror {:?} is not a directory.", path;
493 Invalid, Configuration, File));
494 }
495 match fs::create_dir_all(path) {
496 Ok(()) => (),
497 Err(e) => return Err(err!(e,
498 "The mirror directory {:?} could not be made.", path;
499 IO, File, Write)),
500 }
501 if res!(fs::read_dir(path)).next().is_some() {
502 return Err(err!(
503 "The mirror {:?} holds files and is not a git repository, so this tool \
504 will not make one there.", path;
505 Invalid, Configuration, Conflict));
506 }
507 res!(git(path, &["init", "--bare", "--quiet", "."]));
508 res!(git(path, &["symbolic-ref", "HEAD", &fmt!("refs/heads/{}", BRANCH)]));
509 Ok(())
510}
511
512
513/// Works out what the mirror is missing and writes it.
514fn write_out(repo: &Repo, path: &Path, state: &mut State)
515 -> Outcome<Report>
516{
517 let mut report = Report { path: path.to_path_buf(), ..Report::default() };
518 let frontier = repo.log.frontier();
519
520 // Nothing has changed since the branch was last set, so there is nothing to
521 // say. The render is a function of the log, and the frontier names the log.
522 //
523 // Asked BEFORE the mark graph below rather than after it. The walk was 25 ms
524 // of every `ore log` on a mirrored repository of 35,408 operations, and its
525 // answer was thrown away on the next line.
526 if state.frontier == frontier && state.head.is_some() {
527 return Ok(report);
528 }
529
530 // Every mark, in op order, which respects causality and so is a topological
531 // order of the mark graph.
532 let mut marks: Vec<OpId> = repo.log.iter()
533 .filter(|rec| matches!(rec.op, Op::Mark { .. }))
534 .map(|rec| rec.head.id())
535 .collect();
536 marks.sort_by_key(oxedyne_fe2o3_ore::seq::OpOrder::of);
537 let is_mark: BTreeSet<OpId> = marks.iter().copied().collect();
538
539 // For each mark, the marks nearest to it in the graph and every mark it
540 // reaches. The second is what says which marks are heads, and it is built as
541 // the walk goes so that no mark's ancestry is walked twice.
542 let mut nearest: BTreeMap<OpId, Vec<OpId>> = BTreeMap::new();
543 let mut covers: BTreeMap<OpId, BTreeSet<OpId>> = BTreeMap::new();
544 for id in &marks {
545 let found = res!(nearest_marks(repo, id, &is_mark));
546 let mut reach: BTreeSet<OpId> = BTreeSet::new();
547 for f in &found {
548 reach.insert(*f);
549 if let Some(under) = covers.get(f) {
550 reach.extend(under.iter().copied());
551 }
552 }
553 // A mark the walk found that another found mark already reaches is not
554 // nearest to anything, and git would take it as a redundant parent.
555 let reached_by_others: BTreeSet<OpId> = found.iter()
556 .flat_map(|f| covers.get(f).into_iter().flatten().copied())
557 .collect();
558 let mut direct: Vec<OpId> = found.into_iter()
559 .filter(|f| !reached_by_others.contains(f))
560 .collect();
561 direct.sort_by_key(oxedyne_fe2o3_ore::seq::OpOrder::of);
562 nearest.insert(*id, direct);
563 covers.insert(*id, reach);
564 }
565 let covered: BTreeSet<OpId> = covers.values().flatten().copied().collect();
566 let heads: Vec<OpId> = marks.iter().copied()
567 .filter(|m| !covered.contains(m))
568 .collect();
569
570 let known: BTreeMap<OpId, Exported> = state.marks.iter()
571 .map(|m| (m.mark, m.clone()))
572 .collect();
573 let fresh: Vec<OpId> = marks.iter().copied()
574 .filter(|m| !known.contains_key(m))
575 .collect();
576
577 // The tail is empty when every operation nothing builds on is itself a mark.
578 let tail_empty = frontier.iter().all(|id| is_mark.contains(id));
579 let now = res!(SystemTime::now().duration_since(UNIX_EPOCH)).as_secs() as i64;
580
581 // A mark's tree is wanted twice: as its own, and as the difference the marks
582 // built on it are taken against. It is therefore rendered once and held --
583 // but only until the last mark that will ask for it has been written.
584 //
585 // Keeping every one of them instead is what made a mirror of a real
586 // repository impossible rather than slow. An import mints a mark per commit,
587 // a tree holds an entry per path, and fe2o3's 539 marks come to 10.9 GB of
588 // tree bytes summed; the run was killed by the kernel having written no
589 // objects and no refs. That is the same fault the import itself had, fixed
590 // there the same way -- see `needed` in [`crate::gitimport`].
591 let mut trees: BTreeMap<OpId, gitexport::Tree> = BTreeMap::new();
592 // How many marks still to be written will take each tree as their base.
593 let mut wanted: BTreeMap<OpId, usize> = BTreeMap::new();
594 for id in &fresh {
595 if let Some(first) = nearest.get(id).and_then(|p| p.first()) {
596 *wanted.entry(*first).or_default() += 1;
597 }
598 }
599 let mut stream = Stream::new();
600 let mut minted: BTreeMap<OpId, u64> = BTreeMap::new();
601 // What each commit was dated, so that the record written at the end says the
602 // same thing the commit does rather than reading the clock a second time.
603 let mut dated: BTreeMap<OpId, i64> = BTreeMap::new();
604 let mut next_mark: u64 = 1;
605 let tags = res!(tag_names(repo, &marks));
606
607 for id in &fresh {
608 let here = res!(rendered(repo, id, &mut trees));
609 // Nothing still to come is written against this one, so holding it would
610 // be holding it for the rest of the run.
611 if !wanted.contains_key(id) {
612 trees.remove(id);
613 }
614 let parents = match nearest.get(id) {
615 Some(p) => p.clone(),
616 None => Vec::new(),
617 };
618 let base = match parents.first() {
619 Some(first) => {
620 let base = res!(rendered(repo, first, &mut trees));
621 // One fewer mark will ask for it, and at zero it is let go of.
622 if let Some(left) = wanted.get_mut(first) {
623 *left -= 1;
624 if *left == 0 {
625 wanted.remove(first);
626 trees.remove(first);
627 }
628 }
629 base
630 },
631 None => gitexport::Tree::new(),
632 };
633 let changes = match gitexport::changes(&base, &here) {
634 Ok(c) => c,
635 Err(e) => return Err(err!(e,
636 "The state at mark {} is not one a git repository can hold, so the \
637 mirror stops here rather than holding something else.", id;
638 Invalid, Data)),
639 };
640 let named: Vec<ObjRef> = parents.iter()
641 .map(|p| match minted.get(p) {
642 Some(n) => ObjRef::Mark(*n),
643 None => match known.get(p) {
644 Some(done) => ObjRef::Name(done.commit.clone()),
645 None => ObjRef::Mark(0),
646 },
647 })
648 .collect();
649 if named.iter().any(|r| *r == ObjRef::Mark(0)) {
650 return Err(err!(
651 "The mark {} builds on a mark this mirror has never written, which \
652 cannot happen while marks are written in op order.", id;
653 Bug, Missing));
654 }
655 let mark = res!(marked(repo, id));
656 let mut message = mark.name.into_bytes();
657 message.push(b'\n');
658 if !mark.body.is_empty() {
659 message.push(b'\n');
660 message.extend_from_slice(&mark.body);
661 if !mark.body.ends_with(b"\n") {
662 message.push(b'\n');
663 }
664 }
665 message.extend_from_slice(fmt!("\nOre-Mark: {}\n", id).as_bytes());
666 let secs = match mark.time {
667 Some(secs) => secs.min(i64::MAX as u64) as i64,
668 None => known.get(id).map(|k| k.secs).unwrap_or(now),
669 };
670 dated.insert(*id, secs);
671 res!(stream.commit(&Commit {
672 refname: fmt!("{}", STAGING),
673 mark: Some(next_mark),
674 // Absent unless an import recorded a line, and git then takes the
675 // committer, which is what a mark written in Ore itself wants.
676 author: mark.author.as_deref().and_then(|line| person(line, secs)),
677 committer: who(repo, id.replica.inner(), secs),
678 message,
679 from: named.first().cloned(),
680 merges: named.iter().skip(1).cloned().collect(),
681 changes,
682 }));
683 minted.insert(*id, next_mark);
684 next_mark += 1;
685 report.commits += 1;
686 }
687
688 // The branch. Where the working copy stands exactly at one mark, the branch
689 // points at that mark's own commit; otherwise a commit is made for the tail,
690 // parented on where the branch already stood so that it fast-forwards.
691 let at_one_mark = tail_empty && heads.len() == 1;
692 let straight = at_one_mark && match (heads.first(), state.head_mark) {
693 (Some(_), None) => state.head.is_none(),
694 (Some(m), Some(was)) => was == *m
695 || covers.get(m).is_some_and(|reach| reach.contains(&was)),
696 _ => false,
697 };
698 let branch = fmt!("refs/heads/{}", BRANCH);
699 let head_ref: ObjRef;
700 if straight {
701 let m = match heads.first() {
702 Some(m) => *m,
703 None => return Err(err!("A single mark head was counted and not found."; Bug)),
704 };
705 head_ref = res!(refer(&m, &minted, &known));
706 state.head_mark = Some(m);
707 } else {
708 let whole = res!(tree::whole(&repo.log));
709 let here = res!(git_tree(&whole));
710 // A commit for the tail restates the difference against wherever the
711 // branch already stood, and where it stood is the render at the frontier
712 // the last run recorded. The render is a function of the log, so that
713 // tree is recoverable rather than remembered.
714 let base = match &state.head {
715 Some(_) if !state.frontier.is_empty() =>
716 res!(git_tree(&res!(tree::at(&repo.log, &state.frontier)))),
717 _ => gitexport::Tree::new(),
718 };
719 let changes = match gitexport::changes(&base, &here) {
720 Ok(c) => c,
721 Err(e) => return Err(err!(e,
722 "The state of the working copy is not one a git repository can hold, \
723 so the mirror stops here rather than holding something else.";
724 Invalid, Data)),
725 };
726 // Where the branch already stands is the first parent, so it moves
727 // forward and never sideways; every mark head the branch does not
728 // already reach joins it.
729 let mut parents: Vec<ObjRef> = Vec::new();
730 match &state.head {
731 Some(head) => {
732 parents.push(ObjRef::Name(head.clone()));
733 for m in &heads {
734 if minted.contains_key(m) {
735 parents.push(res!(refer(m, &minted, &known)));
736 }
737 }
738 },
739 None => for m in &heads {
740 parents.push(res!(refer(m, &minted, &known)));
741 },
742 }
743 let mut message = fmt!("Working state").into_bytes();
744 message.extend_from_slice(b"\n\nOre-Frontier:");
745 for id in &frontier {
746 message.extend_from_slice(fmt!(" {}", id).as_bytes());
747 }
748 message.push(b'\n');
749 res!(stream.commit(&Commit {
750 refname: fmt!("{}", STAGING),
751 mark: Some(next_mark),
752 author: None,
753 committer: who(repo, repo.cfg.replica.inner(), now),
754 message,
755 from: parents.first().cloned(),
756 merges: parents.iter().skip(1).cloned().collect(),
757 changes,
758 }));
759 head_ref = ObjRef::Mark(next_mark);
760 state.head_mark = None;
761 report.tail = true;
762 }
763 res!(stream.reset(&branch, Some(&head_ref)));
764 report.moved = true;
765
766 // A tag for every mark this run wrote. Where one name has been marked twice,
767 // the tag stands where the name was last used, which is how `ore back` reads
768 // it, and it is the only reference besides the branch that ever moves.
769 let mut owner: BTreeMap<String, OpId> = BTreeMap::new();
770 for id in &marks {
771 if let Some(tag) = tags.get(id) {
772 owner.insert(tag.clone(), *id);
773 }
774 }
775 for (tag, id) in &owner {
776 let already = known.get(id).is_some_and(|k| k.tag == *tag);
777 if already && !minted.contains_key(id) {
778 continue;
779 }
780 res!(stream.reset(
781 &fmt!("refs/tags/{}", tag), Some(&res!(refer(id, &minted, &known)))));
782 report.tags += 1;
783 }
784 stream.done();
785
786 let names = res!(feed(path, stream.bytes()));
787 // What git called each commit, so that the next run can build on it.
788 for id in &fresh {
789 let n = match minted.get(id) {
790 Some(n) => n,
791 None => continue,
792 };
793 let commit = match names.get(n) {
794 Some(c) => c.clone(),
795 None => return Err(err!(
796 "git wrote the mark {} and did not say what it called the commit.", id;
797 Bug, Missing)),
798 };
799 let secs = dated.get(id).copied().unwrap_or(now);
800 state.marks.push(Exported { mark: *id, secs, tag: String::new(), commit });
801 }
802 // A tag names one commit, so a mark whose name has since been used again
803 // holds none.
804 for done in state.marks.iter_mut() {
805 done.tag = match tags.get(&done.mark) {
806 Some(tag) if owner.get(tag) == Some(&done.mark) => tag.clone(),
807 _ => String::new(),
808 };
809 }
810 state.marks.sort_by_key(|m| m.mark);
811 state.head = Some(res!(head_of(path, &branch)));
812 state.frontier = frontier;
813 let _ = git(path, &["update-ref", "-d", STAGING]);
814 Ok(report)
815}
816
817/// Returns how the stream refers to a mark's commit.
818fn refer(
819 id: &OpId,
820 minted: &BTreeMap<OpId, u64>,
821 known: &BTreeMap<OpId, Exported>,
822)
823 -> Outcome<ObjRef>
824{
825 match minted.get(id) {
826 Some(n) => Ok(ObjRef::Mark(*n)),
827 None => match known.get(id) {
828 Some(done) => Ok(ObjRef::Name(done.commit.clone())),
829 None => Err(err!(
830 "The mark {} has no commit in the mirror and none was just written.", id;
831 Bug, Missing)),
832 },
833 }
834}
835
836/// Returns the marks nearest to an operation: those reached by walking back
837/// through its parents and stopping at the first mark on every path.
838fn nearest_marks(repo: &Repo, from: &OpId, is_mark: &BTreeSet<OpId>)
839 -> Outcome<Vec<OpId>>
840{
841 let mut found: BTreeSet<OpId> = BTreeSet::new();
842 let mut seen: BTreeSet<OpId> = BTreeSet::new();
843 let mut stack: Vec<OpId> = match repo.log.get(from) {
844 Some(rec) => rec.parents().to_vec(),
845 None => return Err(err!(
846 "The log does not hold the operation {}.", from;
847 Bug, Missing)),
848 };
849 while let Some(next) = stack.pop() {
850 if !seen.insert(next) {
851 continue;
852 }
853 if is_mark.contains(&next) {
854 found.insert(next);
855 continue;
856 }
857 match repo.log.get(&next) {
858 Some(rec) => stack.extend(rec.parents().iter().copied()),
859 None => return Err(err!(
860 "The log does not hold the operation {}, which {} builds on.", next, from;
861 Invalid, Data, Missing)),
862 }
863 }
864 Ok(found.into_iter().collect())
865}
866
867/// What a mark says about itself, in the shape a commit wants it.
868struct Marked {
869 name: String,
870 body: Vec<u8>, // the message body, the identity trailer taken off
871 author: Option<String>, // the identity line the trailer carried
872 time: Option<u64>, // unix epoch seconds, where the mark carries one
873}
874
875/// Returns what a mark says about itself.
876fn marked(repo: &Repo, id: &OpId)
877 -> Outcome<Marked>
878{
879 match repo.log.get(id) {
880 Some(rec) => match &rec.op {
881 Op::Mark { name, body, time } => {
882 // Borrowed upstream and owned here, since `Marked` outlives the
883 // record it was read from.
884 let (body, author) = match body {
885 Some(said) => {
886 let (body, who) = without_author(said);
887 (body.to_vec(),
888 who.map(|w| String::from_utf8_lossy(w).into_owned()))
889 },
890 None => (Vec::new(), None),
891 };
892 Ok(Marked { name: name.clone(), body, author, time: *time })
893 },
894 other => Err(err!(
895 "The operation {} is a {} and not a mark.", id, other.name();
896 Bug, Mismatch)),
897 },
898 None => Err(err!("The log does not hold the mark {}.", id; Bug, Missing)),
899 }
900}
901
902/// Returns the name a mark was given.
903fn mark_name(repo: &Repo, id: &OpId)
904 -> Outcome<String>
905{
906 Ok(res!(marked(repo, id)).name)
907}
908
909/// Returns the tag every mark takes, which is its name made into something git
910/// will accept.
911///
912/// Two different names that come out the same way both take the mark's identity
913/// as well, so that the answer is a function of the operation set and neither of
914/// them quietly becomes the other. One name marked twice keeps one tag, which
915/// stands where the name was last used, exactly as `ore back` reads it.
916fn tag_names(repo: &Repo, marks: &[OpId])
917 -> Outcome<BTreeMap<OpId, String>>
918{
919 let mut by_tag: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
920 let mut named: Vec<(OpId, String, String)> = Vec::new();
921 for id in marks {
922 let name = res!(mark_name(repo, id));
923 // `git fast-import` refuses the whole stream over one reference it cannot
924 // create, and the verb it was run from still succeeds -- so a name too
925 // long to be a file name takes the mirror down and says so on stderr
926 // alone. Two names cut to the same tag are two names sharing a tag, which
927 // is decided below and needs no rule of its own.
928 let tag = shorten(&gitexport::sanitise_refname(&name));
929 by_tag.entry(tag.clone()).or_default().insert(name.clone());
930 named.push((*id, name, tag));
931 }
932 let mut out = BTreeMap::new();
933 for (id, _, tag) in named {
934 let shared = by_tag.get(&tag).map(|s| s.len()).unwrap_or(1) > 1;
935 let final_tag = if shared {
936 fmt!("{}-r{}-{}", tag, id.replica, id.counter)
937 } else {
938 tag
939 };
940 match gitexport::check_refname(&fmt!("refs/tags/{}", final_tag)) {
941 Ok(()) => (),
942 Err(e) => return Err(err!(e,
943 "The mark {} cannot be named as a git tag.", id;
944 Invalid, Data)),
945 }
946 out.insert(id, final_tag);
947 }
948 Ok(out)
949}
950
951/// Cuts a tag to something git can name a file, on a character boundary.
952///
953/// Sanitised again afterwards, since a cut can leave the trailing dot that
954/// [`gitexport::sanitise_refname`] exists to take off.
955fn shorten(tag: &str) -> String {
956 if tag.len() <= TAG_LIMIT {
957 return fmt!("{}", tag);
958 }
959 let at = tag.char_indices()
960 .map(|(at, _)| at)
961 .take_while(|at| *at <= TAG_LIMIT)
962 .last()
963 .unwrap_or(0);
964 gitexport::sanitise_refname(&tag[..at])
965}
966
967/// Returns the tree a mark stands for, rendering it if it has not been.
968fn rendered<'a>(repo: &Repo, id: &OpId, cache: &'a mut BTreeMap<OpId, gitexport::Tree>)
969 -> Outcome<gitexport::Tree>
970{
971 if let Some(held) = cache.get(id) {
972 return Ok(held.clone());
973 }
974 let tree = res!(tree::at(&repo.log, &[*id]));
975 let built = res!(git_tree(&tree));
976 cache.insert(*id, built.clone());
977 Ok(built)
978}
979
980/// Turns a render into the tree a git commit holds.
981///
982/// The layout is the working copy's own, so a path two live files claim resolves
983/// in the mirror exactly as it resolves on disk, and the two never disagree about
984/// which file a name stands for.
985fn git_tree(tree: &tree::Tree)
986 -> Outcome<gitexport::Tree>
987{
988 let layout = res!(tree.layout());
989 let mut out = gitexport::Tree::new();
990 for (path, file) in &layout.at {
991 let view = match tree.repo.file(*file) {
992 Some(v) => v,
993 None => return Err(err!(
994 "The layout places the file {}, which the render does not hold.", file;
995 Bug, Missing)),
996 };
997 out.insert(path.clone(), Entry {
998 mode: mode_of(view.mode()),
999 data: view.bytes().to_vec(),
1000 });
1001 }
1002 Ok(out)
1003}
1004
1005/// Returns the git mode a file's mode is spelled as.
1006fn mode_of(mode: Mode) -> FileMode {
1007 match mode {
1008 Mode::Normal => FileMode::Normal,
1009 Mode::Executable => FileMode::Executable,
1010 Mode::Symlink => FileMode::Symlink,
1011 }
1012}
1013
1014/// Returns the identity line a replica's commits are written under.
1015///
1016/// An import recorded the git identity every author came in as, so a history
1017/// that began as a git repository goes back out under the names it arrived with.
1018/// A replica nobody has a name for is written as its number, in a domain
1019/// reserved so that it can never be a real address: git wants a name, and
1020/// inventing a person is worse than admitting there is not one.
1021fn who(repo: &Repo, replica: u64, secs: i64) -> Person {
1022 // Which of a replica's identity lines this finds is settled by byte order and
1023 // means nothing. It is the right answer only for a mark that carries no line
1024 // of its own, which is every mark written in Ore rather than imported.
1025 for (identity, id) in &repo.cfg.authors {
1026 if *id != replica {
1027 continue;
1028 }
1029 if let Some(found) = person(identity, secs) {
1030 return found;
1031 }
1032 }
1033 Person {
1034 name: fmt!("replica {}", replica).into_bytes(),
1035 email: fmt!("{}@{}", replica, NO_DOMAIN).into_bytes(),
1036 when: stamp(secs),
1037 }
1038}
1039
1040/// Returns the person an author line names, at the moment the caller gives.
1041///
1042/// The line is git's own -- `Name <email> 1735089438 +0800` -- where an import
1043/// wrote it, and bare `Name <email>` where it came from the author map. Only the
1044/// offset is taken from the line. **The instant is always the caller's**, which
1045/// is the mark's own time and inside the signature; a trailer is not, so reading
1046/// the seconds here would let something nobody signed move a commit's date. They
1047/// are parsed all the same, because the identity is what is in front of them, and
1048/// [`split_identity_line`] is where that is decided rather than here -- one rule
1049/// with a mirror and a forge reading it.
1050///
1051/// `None` only where the identity itself will not parse, which sends the caller
1052/// back to [`who`]. A line naming somebody with no moment after them keeps the
1053/// name and takes [`UNZONED`], since losing the person over a missing offset
1054/// would be losing the thing the trailer is for.
1055fn person(line: &str, secs: i64) -> Option<Person> {
1056 let (identity, tz) = match split_identity_line(line) {
1057 Some((identity, when)) => (identity, when.tz),
1058 None => (line, UNZONED),
1059 };
1060 let (name, rest) = match identity.rsplit_once(" <") {
1061 Some(split) => split,
1062 None => return None,
1063 };
1064 let email = match rest.strip_suffix('>') {
1065 Some(email) => email,
1066 None => return None,
1067 };
1068 Some(Person {
1069 name: name.as_bytes().to_vec(),
1070 email: email.as_bytes().to_vec(),
1071 when: When { secs, tz },
1072 })
1073}
1074
1075/// Returns a moment as an identity line spells one, in a zone nobody recorded.
1076fn stamp(secs: i64) -> When {
1077 When { secs, tz: UNZONED }
1078}
1079
1080
1081// ---------------------------------------------------------------------------
1082// Running git.
1083// ---------------------------------------------------------------------------
1084
1085/// Runs git in a directory and returns its standard output.
1086fn git(dir: &Path, args: &[&str])
1087 -> Outcome<Vec<u8>>
1088{
1089 let out = match Command::new("git").current_dir(dir).args(args).output() {
1090 Ok(o) => o,
1091 Err(e) => return Err(err!(e,
1092 "`git {}` could not be run in {:?}. A mirror needs a git binary on the \
1093 path.", args.join(" "), dir;
1094 IO, System)),
1095 };
1096 if !out.status.success() {
1097 return Err(err!(
1098 "`git {}` failed in {:?}: {}", args.join(" "), dir,
1099 String::from_utf8_lossy(&out.stderr).trim();
1100 IO, System));
1101 }
1102 Ok(out.stdout)
1103}
1104
1105/// Returns the object name a reference holds.
1106fn head_of(dir: &Path, refname: &str)
1107 -> Outcome<String>
1108{
1109 let out = res!(git(dir, &["rev-parse", "--verify", refname]));
1110 Ok(fmt!("{}", String::from_utf8_lossy(&out).trim()))
1111}
1112
1113/// Applies a stream to the mirror, returning what git named each mark.
1114///
1115/// Git is run with `--done`, so a stream cut short is refused rather than half
1116/// applied: what comes back is either the whole of it or an error.
1117fn feed(dir: &Path, stream: &[u8])
1118 -> Outcome<BTreeMap<u64, String>>
1119{
1120 let marks = dir.join(MARK_FILE);
1121 let named = fmt!("{}", marks.display());
1122 let mut child = match Command::new("git")
1123 .current_dir(dir)
1124 .args(["fast-import", "--quiet", "--done", &fmt!("--export-marks={}", named)])
1125 .stdin(Stdio::piped())
1126 .stdout(Stdio::piped())
1127 .stderr(Stdio::piped())
1128 .spawn()
1129 {
1130 Ok(c) => c,
1131 Err(e) => return Err(err!(e,
1132 "`git fast-import` could not be run in {:?}. A mirror needs a git binary \
1133 on the path.", dir;
1134 IO, System)),
1135 };
1136 match child.stdin.take() {
1137 Some(mut pipe) => match pipe.write_all(stream) {
1138 Ok(()) => (),
1139 Err(e) => return Err(err!(e,
1140 "The stream could not be handed to `git fast-import`.";
1141 IO, Write)),
1142 },
1143 None => return Err(err!(
1144 "`git fast-import` gave no standard input to write to.";
1145 Bug, IO)),
1146 }
1147 let out = match child.wait_with_output() {
1148 Ok(o) => o,
1149 Err(e) => return Err(err!(e,
1150 "`git fast-import` could not be waited for.";
1151 IO, System)),
1152 };
1153 if !out.status.success() {
1154 return Err(err!(
1155 "`git fast-import` refused the mirror's stream: {}",
1156 String::from_utf8_lossy(&out.stderr).trim();
1157 IO, System));
1158 }
1159 let text = match fs::read_to_string(&marks) {
1160 Ok(t) => t,
1161 Err(e) => return Err(err!(e,
1162 "git wrote no record of what it named each commit, at {:?}.", marks;
1163 IO, File, Read)),
1164 };
1165 let _ = fs::remove_file(&marks);
1166 let mut out = BTreeMap::new();
1167 for line in text.lines() {
1168 let (mark, commit) = match line.split_once(' ') {
1169 Some(pair) => pair,
1170 None => continue,
1171 };
1172 let n = match mark.trim_start_matches(':').parse::<u64>() {
1173 Ok(n) => n,
1174 Err(e) => return Err(err!(e,
1175 "git named a mark {:?}, which is not a number.", mark;
1176 Decode, Input)),
1177 };
1178 out.insert(n, fmt!("{}", commit.trim()));
1179 }
1180 Ok(out)
1181}