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 | |
| 93 | use crate::repo::{ |
| 94 | Config, |
| 95 | Repo, |
| 96 | }; |
| 97 | use crate::tree; |
| 98 | |
| 99 | use oxedyne_fe2o3_core::prelude::*; |
| 100 | use oxedyne_fe2o3_jdat::prelude::*; |
| 101 | use oxedyne_fe2o3_ore::fastexport::{ |
| 102 | split_identity_line, |
| 103 | FileMode, |
| 104 | ObjRef, |
| 105 | Person, |
| 106 | TzOffset, |
| 107 | When, |
| 108 | }; |
| 109 | use oxedyne_fe2o3_ore::gitexport::{ |
| 110 | self, |
| 111 | Commit, |
| 112 | Entry, |
| 113 | Stream, |
| 114 | }; |
| 115 | use oxedyne_fe2o3_ore::id::OpId; |
| 116 | use oxedyne_fe2o3_ore::op::{ |
| 117 | without_author, |
| 118 | Mode, |
| 119 | Op, |
| 120 | }; |
| 121 | |
| 122 | use std::collections::{ |
| 123 | BTreeMap, |
| 124 | BTreeSet, |
| 125 | }; |
| 126 | use std::fs; |
| 127 | use std::io::Write; |
| 128 | use std::path::{ |
| 129 | Path, |
| 130 | PathBuf, |
| 131 | }; |
| 132 | use std::process::{ |
| 133 | Command, |
| 134 | Stdio, |
| 135 | }; |
| 136 | use std::time::{ |
| 137 | SystemTime, |
| 138 | UNIX_EPOCH, |
| 139 | }; |
| 140 | |
| 141 | |
| 142 | /// What the mirror's own bookkeeping is called, inside `.ore`. |
| 143 | pub const STATE_FILE: &str = "mirror"; |
| 144 | |
| 145 | /// Version of that bookkeeping's format. |
| 146 | pub const STATE_VERSION: u64 = 1; |
| 147 | |
| 148 | /// The branch a reader clones. |
| 149 | pub 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. |
| 158 | const 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. |
| 166 | const 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. |
| 170 | pub const STAGING: &str = "refs/ore/staging"; |
| 171 | |
| 172 | /// Where git writes back the object name of every mark the stream minted. |
| 173 | const 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. |
| 177 | const 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)] |
| 186 | pub 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 | |
| 196 | impl 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)] |
| 233 | pub 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 | |
| 246 | impl 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. |
| 319 | fn 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`]. |
| 336 | fn 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`]. |
| 348 | fn 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)] |
| 370 | pub 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 | |
| 383 | impl 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. |
| 398 | pub 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. |
| 422 | pub 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. |
| 432 | pub 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. |
| 443 | pub 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. |
| 466 | pub 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. |
| 484 | fn 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. |
| 514 | fn 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. |
| 818 | fn 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. |
| 838 | fn 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. |
| 868 | struct 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. |
| 876 | fn 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. |
| 903 | fn 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. |
| 916 | fn 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. |
| 955 | fn 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. |
| 968 | fn 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. |
| 985 | fn 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. |
| 1006 | fn 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. |
| 1021 | fn 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. |
| 1055 | fn 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. |
| 1076 | fn 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. |
| 1086 | fn 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. |
| 1106 | fn 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. |
| 1117 | fn 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 | } |