OXYGENHUB
◆ v0.9.0 oxygen / crates / oxy-core / src / repo / strata.rs
51.8 KiB · Rust · last changed A shallow clone fetches its checkpoint in parts (refs #137) 10 days ago source blame history raw
9f9e72b9 Andy McCall ↶ 1
//! A repository kept on a [`Store`] (Oxygen Strata, docs/stores.md): what
2
//! `oxy push dir://…` or `oxy clone s3://…` talk to.
3
//!
4
//! The store holds three kinds of key:
5
//!
6
//! - `oxygen.toml`: the layout's format and the repository's id, written
7
//!   once by the first push;
8
//! - `bundles/<seq>-<hash>.bundle`: each push's changes, signatures,
9
//!   seals, objects and policies, as the wire protocol encodes a bundle,
10
//!   written once and named by content;
11
//! - `refs`: every bookmark with its heads and attestation, every tag, the
12
//!   policy and its history, encoded as the protocol's `refs` response,
13
//!   moved by compare-and-swap.
14
//!
15
//! A store cannot run code, so the client does what a hub does on a push,
16
//! against a **mirror**: a bare repository of its own, kept beside the
17
//! remote's other state under `.oxy/remotes/<name>/strata/`, brought level
18
//! with the store before every use (the record read, the bundles it has
19
//! not seen imported, the view set from the record), and then used as a
20
//! [`LocalRemote`] would be. A push runs the hub's own checks
21
//! (`receive_push`) on the mirror, uploads the bundle, and moves `refs`
22
//! from the version it read; if the record moved meanwhile, the mirror is
23
//! put back and the push reports it, exactly as a hub reports a stale
24
//! push. Nothing in the store is trusted: every object is re-hashed as it
25
//! is imported, and the fetch that follows verifies each bookmark's
26
//! attestation under the policy, as it does for any remote.
27
28
use super::{fail, Repo, RepoError, Result};
29
use crate::hash::{ChangeHash, ObjectRef, ObjectType};
30
use crate::objects::StateObject;
31
use crate::protocol::{
32
    wire, Bundle, RefResult, RefUpdate, RefsResponse, Remote, RemoteOptions, SignedPolicy, TagRef,
33
    UNBOUND_ID,
34
};
35
use crate::strata::{Error as StoreError, Store, Version};
36
use std::cell::RefCell;
37
use std::collections::BTreeSet;
38
use std::fs;
39
use std::path::{Path, PathBuf};
40
41
const MARKER_KEY: &str = "oxygen.toml";
42
const REFS_KEY: &str = "refs";
43
const BUNDLES_PREFIX: &str = "bundles/";
44
const FORMAT: u32 = 1;
45
46
impl From<StoreError> for RepoError {
47
    fn from(e: StoreError) -> RepoError {
48
        RepoError::Msg(e.to_string())
49
    }
50
}
51
52
/// The `oxygen.toml` marker: two lines, written once.
53
fn marker(id: [u8; 16]) -> Vec<u8> {
54
    format!("format = {FORMAT}\nrepo = \"{}\"\n", hex::encode(id)).into_bytes()
55
}
56
57
fn parse_marker(bytes: &[u8], what: &str) -> Result<[u8; 16]> {
58
    let text = String::from_utf8_lossy(bytes);
59
    let mut format = None;
60
    let mut repo = None;
61
    for line in text.lines() {
62
        let Some((k, v)) = line.split_once('=') else {
63
            continue;
64
        };
65
        let v = v.trim().trim_matches('"');
66
        match k.trim() {
67
            "format" => format = v.parse::<u32>().ok(),
68
            "repo" => {
69
                repo = hex::decode(v)
70
                    .ok()
71
                    .and_then(|b| <[u8; 16]>::try_from(b).ok())
72
            }
73
            _ => {}
74
        }
75
    }
76
    match (format, repo) {
77
        (Some(f), _) if f > FORMAT => fail(format!(
78
            "{what} holds a repository in Strata format {f}, newer than this oxy understands (format {FORMAT}): upgrade oxy"
79
        )),
80
        (Some(_), Some(id)) => Ok(id),
81
        _ => fail(format!("{what}: its oxygen.toml is not a Strata marker")),
82
    }
83
}
84
85
/// A remote kept on a store.
86
pub struct BucketRemote {
87
    store: Box<dyn Store>,
88
    /// Where the mirror lives: the remote's directory under `.oxy/remotes`,
89
    /// or a temporary directory when there is no repository yet (a clone),
90
    /// removed on drop.
91
    dir: PathBuf,
92
    temporary: bool,
93
    claimed_id: Option<[u8; 16]>,
94
    mirror: RefCell<Option<Repo>>,
95
}
96
97
impl Drop for BucketRemote {
98
    fn drop(&mut self) {
99
        if self.temporary {
100
            let _ = fs::remove_dir_all(&self.dir);
101
        }
102
    }
103
}
104
105
/// What a sync found: the record's version (to move it from) and the
106
/// repository id the store holds, if any.
107
struct Synced {
108
    version: Option<Version>,
109
    id: Option<[u8; 16]>,
110
}
111
112
impl BucketRemote {
113
    pub fn open(store: Box<dyn Store>, options: &RemoteOptions) -> Result<BucketRemote> {
114
        let (dir, temporary) = match &options.cache_dir {
115
            Some(d) => (d.clone(), false),
116
            None => {
117
                let mut nonce = [0u8; 6];
118
                rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut nonce);
119
                (
120
                    std::env::temp_dir().join(format!(
121
                        "oxy-strata-{}-{}",
122
                        std::process::id(),
123
                        hex::encode(nonce)
124
                    )),
125
                    true,
126
                )
127
            }
128
        };
129
        Ok(BucketRemote {
130
            store,
131
            dir,
132
            temporary,
133
            claimed_id: options.repo_id,
134
            mirror: RefCell::new(None),
135
        })
136
    }
137
138
    fn what(&self) -> String {
139
        self.store.describe()
140
    }
141
142
    /// The repository id the store holds: `None` until a first push.
143
    fn stored_id(&self) -> Result<Option<[u8; 16]>> {
144
        match self.store.get(MARKER_KEY) {
145
            Ok(b) => Ok(Some(parse_marker(&b, &self.what())?)),
146
            Err(StoreError::NotFound(_)) => Ok(None),
147
            Err(e) => Err(e.into()),
148
        }
149
    }
150
151
    fn mirror_dir(&self) -> PathBuf {
152
        self.dir.join("mirror")
153
    }
154
155
    fn seen_path(&self) -> PathBuf {
156
        self.dir.join("seen")
157
    }
158
159
    /// The bundles already imported into the mirror.
160
    fn seen(&self) -> BTreeSet<String> {
161
        fs::read_to_string(self.seen_path())
162
            .map(|s| s.lines().map(str::to_string).collect())
163
            .unwrap_or_default()
164
    }
165
166
    fn mark_seen(&self, key: &str) -> Result<()> {
167
        let mut seen = self.seen();
168
        if seen.insert(key.to_string()) {
169
            let text: String = seen.iter().map(|k| format!("{k}\n")).collect();
170
            crate::durable::write_atomic(&self.seen_path(), text.as_bytes())?;
171
        }
172
        Ok(())
173
    }
174
175
    /// Open the mirror for repository `id`, creating it on first use.
176
    fn open_mirror(&self, id: [u8; 16]) -> Result<()> {
177
        if self.mirror.borrow().is_some() {
178
            return Ok(());
179
        }
180
        let dir = self.mirror_dir();
181
        let repo = if dir.join(super::OXY_DIR).is_dir() {
182
            let repo = Repo::open(&dir)?;
183
            if repo.id()? != id {
184
                // The store was emptied and reused for another repository:
185
                // start the mirror over.
186
                drop(repo);
187
                fs::remove_dir_all(&self.dir)?;
188
                fs::create_dir_all(&self.dir)?;
189
                Repo::init_bare_with_id(&dir, id)?
190
            } else {
191
                repo
192
            }
193
        } else {
194
            fs::create_dir_all(&self.dir)?;
195
            Repo::init_bare_with_id(&dir, id)?
196
        };
197
        *self.mirror.borrow_mut() = Some(repo);
198
        Ok(())
199
    }
200
201
    fn with_mirror<T>(&self, f: impl FnOnce(&Repo) -> Result<T>) -> Result<T> {
202
        let m = self.mirror.borrow();
203
        match m.as_ref() {
204
            Some(repo) => f(repo),
205
            None => fail(format!(
206
                "{} holds no repository yet: push one to it first",
207
                self.what()
208
            )),
209
        }
210
    }
211
212
    /// Bring the mirror level with the store.
213
    fn sync(&self) -> Result<Synced> {
214
        let Some(id) = self.stored_id()? else {
215
            return Ok(Synced {
216
                version: None,
217
                id: None,
218
            });
219
        };
220
        self.open_mirror(id)?;
221
        let record = self.store.read_ref(REFS_KEY)?;
222
        let (resp, version) = match record {
223
            Some((bytes, version)) => {
224
                let (rid, resp) = wire::decode_refs(&bytes)?;
225
                if rid != id {
226
                    return fail(format!(
227
                        "{}: its refs record names a different repository than its oxygen.toml; the store is damaged",
228
                        self.what()
229
                    ));
230
                }
231
                (resp, Some(version))
232
            }
233
            None => (RefsResponse::default(), None),
234
        };
235
        self.with_mirror(|mirror| {
767ac6cd Andy McCall 236
            // Another process may have brought the mirror level since this
237
            // handle read its head.
238
            mirror.refresh()?;
9f9e72b9 Andy McCall ↶ 239
            // The policy first: bundles are then imported under it.
240
            if let Some(sp) = &resp.policy {
241
                let have = mirror.current_policy()?.map(|(_, p)| p.policy.serial);
242
                if have.is_none_or(|s| s < sp.policy.serial) {
243
                    mirror
244
                        .adopt_policy_chain(&resp.history, sp, "strata sync")
245
                        .map_err(|e| RepoError::Msg(format!("policy from {}: {e}", self.what())))?;
246
                }
247
            }
248
            self.import_new_bundles(mirror)?;
249
            self.set_view(mirror, &resp)?;
250
            Ok(())
251
        })?;
252
        Ok(Synced {
253
            version,
254
            id: Some(id),
255
        })
256
    }
257
258
    /// Import every bundle in the store the mirror has not seen. Bundles
259
    /// are named in the order they were pushed, but two pushes from the
260
    /// same base may interleave, so a bundle whose dependencies are not
261
    /// there yet is tried again after the others.
262
    fn import_new_bundles(&self, mirror: &Repo) -> Result<()> {
263
        let seen = self.seen();
264
        let mut pending: Vec<String> = self
265
            .store
266
            .list(BUNDLES_PREFIX)?
267
            .into_iter()
268
            .map(|e| e.key)
269
            .filter(|k| !seen.contains(k))
270
            .collect();
271
        let strict = mirror.current_policy()?.is_some();
272
        while !pending.is_empty() {
273
            let mut deferred = Vec::new();
274
            let mut last_error = None;
d8d93678 Andy McCall ↶ 275
            let before = pending.len();
9f9e72b9 Andy McCall ↶ 276
            for key in pending.drain(..) {
d8d93678 Andy McCall ↶ 277
                let bytes = match self.store.get(&key) {
278
                    Ok(b) => b,
279
                    // Listed, then gone: a gc merged it into a newer
280
                    // bundle, which this listing holds too.
281
                    Err(StoreError::NotFound(_)) => continue,
282
                    Err(e) => return Err(e.into()),
283
                };
9f9e72b9 Andy McCall ↶ 284
                let bundle = wire::decode_bundle(&bytes)
285
                    .map_err(|e| RepoError::Msg(format!("{}: {key}: {e}", self.what())))?;
286
                match mirror.import_bundle(&bundle, strict) {
287
                    Ok(_) => self.mark_seen(&key)?,
288
                    Err(RepoError::Msg(m)) if m.contains("neither stored nor in the bundle") => {
289
                        last_error = Some(m);
290
                        deferred.push(key);
291
                    }
292
                    Err(e) => return Err(RepoError::Msg(format!("{}: {key}: {e}", self.what()))),
293
                }
294
            }
d8d93678 Andy McCall ↶ 295
            if deferred.len() == before {
9f9e72b9 Andy McCall ↶ 296
                // No progress: something those bundles need was never
297
                // pushed, or a push is still in progress.
298
                return fail(format!(
299
                    "{}: {} cannot be imported ({}); a push may be in progress, so try again, else the store is missing changes",
300
                    self.what(),
301
                    deferred.join(", "),
302
                    last_error.unwrap_or_default()
303
                ));
304
            }
305
            pending = deferred;
306
        }
307
        Ok(())
308
    }
309
310
    /// Make the mirror's view say what the record says.
311
    fn set_view(&self, mirror: &Repo, resp: &RefsResponse) -> Result<()> {
312
        let (op_ref, op) = mirror.head_op()?;
313
        let mut view = op.view.clone();
314
        view.bookmarks.clear();
315
        for r in &resp.refs {
316
            for h in &r.heads {
317
                if !mirror.store.has(h) {
318
                    return fail(format!(
319
                        "{}: bookmark {:?} names change {}, which no bundle there holds; a push may be in progress, so try again",
320
                        self.what(),
321
                        r.name,
322
                        h.short(12)
323
                    ));
324
                }
325
            }
326
            let state = mirror.store.put_state(&StateObject::new(r.heads.clone()))?;
327
            if state != r.state {
328
                return fail(format!(
329
                    "{}: bookmark {:?} names a state that does not match its heads; the store is damaged",
330
                    self.what(),
331
                    r.name
332
                ));
333
            }
334
            if let Some(sa) = &r.attestation {
335
                let current = mirror.latest_ref_attest(&r.name)?.map(|(_, a)| a);
336
                if current.as_ref() != Some(sa) {
337
                    let a = mirror.store.put_attest(&sa.attest)?;
338
                    for sig in &sa.sigs {
339
                        mirror.store.put_signature(&a, sig)?;
340
                    }
341
                    mirror.set_ref_attest(&r.name, &a)?;
342
                }
343
            }
344
            view.bookmarks.insert(r.name.clone(), state);
345
        }
346
        let policy = mirror.current_policy()?.map(|(_, sp)| sp.policy);
347
        for t in &resp.tags {
348
            mirror.store.put_state(&StateObject::new(t.heads.clone()))?;
349
            mirror
1ec6fdbc Andy McCall ↶ 350
                .adopt_tag(
351
                    &mut view,
352
                    t,
353
                    policy.as_ref(),
354
                    &mut super::tags::TagCheck::Mirrored,
355
                )
356
                .map_err(|e| RepoError::Msg(format!("{}: {}", self.what(), e.reason)))?;
9f9e72b9 Andy McCall ↶ 357
        }
358
        if view != op.view {
359
            mirror.record_op(&[op_ref], "strata sync", view)?;
360
        }
361
        Ok(())
362
    }
363
364
    /// The record for the mirror's current references.
365
    fn record(&self, id: [u8; 16]) -> Result<Vec<u8>> {
366
        self.with_mirror(|mirror| {
367
            let mut resp = mirror.remote_refs()?;
368
            // A mirror has no checkout; the store's clone picks a bookmark
369
            // as any other remote's does.
370
            resp.default_bookmark = None;
371
            Ok(wire::encode_refs(id, &resp))
372
        })
373
    }
374
375
    /// Store a bundle under the next sequence number; returns its key.
376
    fn upload(&self, bundle: &Bundle) -> Result<String> {
377
        let bytes = wire::encode_bundle(bundle);
378
        let hash = blake3::hash(&bytes).to_hex();
379
        let seq = self.store.list(BUNDLES_PREFIX)?.len() + 1;
380
        let key = format!("{BUNDLES_PREFIX}{seq:08}-{}.bundle", &hash[..16]);
381
        match self.store.put_new(&key, &bytes) {
382
            Ok(()) | Err(StoreError::Exists(_)) => {}
383
            Err(e) => return Err(e.into()),
384
        }
385
        self.mark_seen(&key)?;
386
        Ok(key)
387
    }
388
}
389
390
impl Remote for BucketRemote {
391
    fn repo_id(&self) -> Result<[u8; 16]> {
392
        Ok(self.stored_id()?.unwrap_or(UNBOUND_ID))
393
    }
394
395
    fn refs(&self) -> Result<RefsResponse> {
396
        let synced = self.sync()?;
397
        if synced.id.is_none() {
398
            return Ok(RefsResponse::default());
399
        }
400
        self.with_mirror(|mirror| {
401
            let mut resp = mirror.remote_refs()?;
402
            resp.default_bookmark = None;
403
            Ok(resp)
404
        })
405
    }
406
407
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
408
        self.with_mirror(|m| m.negotiate(have, want))
409
    }
410
411
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> {
412
        self.with_mirror(|m| m.bundle_raw_for(changes, max_blob))
413
    }
414
415
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> {
416
        self.with_mirror(|m| m.objects_for(refs))
417
    }
418
419
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle> {
420
        self.with_mirror(|m| m.checkpoint_bundle_for(heads))
421
    }
422
80b069fa Andy McCall ↶ 423
    fn checkpoint_index(&self, heads: &[ChangeHash]) -> Result<Bundle> {
424
        self.with_mirror(|m| m.checkpoint_index_for(heads))
425
    }
426
9f9e72b9 Andy McCall ↶ 427
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> {
428
        self.with_mirror(|m| m.shallow_changes(heads, depth))
429
    }
430
431
    fn push(
432
        &self,
433
        bundle: Bundle,
434
        updates: Vec<RefUpdate>,
435
        policy: Option<SignedPolicy>,
436
        tags: Vec<TagRef>,
437
    ) -> Result<Vec<RefResult>> {
438
        let mut synced = self.sync()?;
439
        let id = match synced.id {
440
            Some(id) => id,
441
            None => {
442
                // The first push binds the store to this repository. Two
443
                // first pushes race on the marker; the loser reads the
444
                // winner's and goes on only if it is the same repository.
445
                let Some(id) = self.claimed_id else {
446
                    return fail(format!(
447
                        "{} is empty, and this client did not say which repository it is",
448
                        self.what()
449
                    ));
450
                };
451
                match self.store.put_new(MARKER_KEY, &marker(id)) {
452
                    Ok(()) => {}
453
                    Err(StoreError::Exists(_)) => {}
454
                    Err(e) => return Err(e.into()),
455
                }
456
                synced = self.sync()?;
457
                match synced.id {
458
                    Some(stored) if stored == id => id,
459
                    _ => {
460
                        return fail(format!(
461
                            "{} was just bound to another repository by a concurrent push",
462
                            self.what()
463
                        ))
464
                    }
465
                }
466
            }
467
        };
468
        if let Some(claimed) = self.claimed_id {
469
            if claimed != id {
470
                return fail(format!(
471
                    "{} holds a different repository (id {}) than this one ({}): a store holds one repository; use another prefix or bucket",
472
                    self.what(),
473
                    hex::encode(id),
474
                    hex::encode(claimed)
475
                ));
476
            }
477
        }
d8d93678 Andy McCall ↶ 478
        // The bundle is checked against the mirror and uploaded once; the
479
        // record is then moved from the version read. If it moved
480
        // meanwhile (another push landed), the mirror is put back, brought
481
        // level again, and the updates applied over what landed: two
482
        // pushes to different bookmarks both get through, and one to the
483
        // same bookmark is told it is stale, as a hub would say.
484
        let mut uploaded = false;
485
        for attempt in 0..PUSH_ATTEMPTS {
486
            let (before_ref, before) = self.with_mirror(|m| m.head_op())?;
487
            let results = self.with_mirror(|m| {
488
                m.receive_push(
489
                    bundle.clone(),
490
                    updates.clone(),
491
                    policy.clone(),
492
                    tags.clone(),
493
                )
494
            })?;
495
            if !results.iter().any(|r| r.ok) {
496
                return Ok(results);
497
            }
498
            if !bundle.is_empty() && !uploaded {
499
                self.upload(&bundle)?;
500
                uploaded = true;
501
            }
502
            let record = self.record(id)?;
503
            match self
504
                .store
505
                .write_ref(REFS_KEY, &record, synced.version.as_ref())
506
            {
507
                Ok(_) => return Ok(results),
508
                Err(StoreError::Conflict(_)) => {
509
                    self.with_mirror(|m| {
510
                        let (now, _) = m.head_op()?;
511
                        if now != before_ref {
512
                            m.record_op(
513
                                &[now],
514
                                "strata push undone: the remote moved",
515
                                before.view.clone(),
516
                            )?;
517
                        }
518
                        Ok(())
519
                    })?;
520
                    synced = self.sync()?;
521
                    if attempt + 1 == PUSH_ATTEMPTS {
522
                        return Ok(results
523
                            .into_iter()
524
                            .map(|r| RefResult {
525
                                name: r.name,
526
                                ok: false,
527
                                reason: "the remote kept moving while pushing (other pushes landed first); pull, then push again".into(),
528
                            })
529
                            .collect());
530
                    }
531
                }
532
                Err(e) => return Err(e.into()),
533
            }
9f9e72b9 Andy McCall ↶ 534
        }
d8d93678 Andy McCall ↶ 535
        unreachable!("the loop returns")
536
    }
537
}
538
539
/// How often a push is applied over what landed meanwhile before giving up.
540
const PUSH_ATTEMPTS: usize = 4;
541
542
/// The grace period before `gc` removes a bundle it merged: a reader that
543
/// listed it before the merge can still fetch it.
544
pub const REMOTE_GC_GRACE: std::time::Duration = std::time::Duration::from_secs(7 * 86_400);
545
546
/// Key of the record naming the bundles a gc merged, with when.
547
const TRASH_KEY: &str = "gc";
548
549
/// The superseded bundles and when (milliseconds) each was superseded.
550
type Trash = Vec<(String, u64)>;
551
552
/// What a gc against a store did.
553
#[derive(Clone, Debug, Default, PartialEq, Eq)]
554
pub struct RemoteGc {
555
    /// Bundles folded into one new bundle.
556
    pub merged: usize,
557
    /// The merged bundle's size, bytes.
558
    pub bundle_bytes: usize,
559
    /// Changes in the merged bundle.
560
    pub changes: usize,
561
    /// Superseded bundles removed, past the grace period.
562
    pub deleted: usize,
563
    /// Superseded bundles kept for now by the grace period.
564
    pub deferred: usize,
565
}
566
567
fn now_ms() -> u64 {
568
    std::time::SystemTime::now()
569
        .duration_since(std::time::UNIX_EPOCH)
570
        .map(|d| d.as_millis() as u64)
571
        .unwrap_or(0)
572
}
573
574
impl BucketRemote {
575
    /// The bundles a gc superseded, with when: one `KEY\tMILLIS` per line.
576
    fn trash(&self) -> Result<(Trash, Option<Version>)> {
577
        match self.store.read_ref(TRASH_KEY)? {
578
            Some((bytes, v)) => {
579
                let text = String::from_utf8_lossy(&bytes);
580
                let list = text
581
                    .lines()
582
                    .filter_map(|l| {
583
                        let (k, t) = l.split_once('\t')?;
584
                        Some((k.to_string(), t.parse().ok()?))
585
                    })
586
                    .collect();
587
                Ok((list, Some(v)))
588
            }
589
            None => Ok((Vec::new(), None)),
9f9e72b9 Andy McCall ↶ 590
        }
d8d93678 Andy McCall ↶ 591
    }
592
593
    fn write_trash(&self, list: &[(String, u64)], expected: Option<&Version>) -> Result<Version> {
594
        let text: String = list.iter().map(|(k, t)| format!("{k}\t{t}\n")).collect();
595
        Ok(self.store.write_ref(TRASH_KEY, text.as_bytes(), expected)?)
596
    }
597
598
    /// Fold every bundle into one holding what the record reaches, and
599
    /// remove the bundles that made a gc redundant once `grace` has
600
    /// passed since they were superseded (stores.md §6).
601
    pub fn gc(&self, grace: std::time::Duration) -> Result<RemoteGc> {
602
        let synced = self.sync()?;
603
        let Some(id) = synced.id else {
604
            return fail(format!("{} holds no repository yet", self.what()));
605
        };
606
        let mut report = RemoteGc::default();
607
        let existing: Vec<String> = self
9f9e72b9 Andy McCall ↶ 608
            .store
d8d93678 Andy McCall ↶ 609
            .list(BUNDLES_PREFIX)?
610
            .into_iter()
611
            .map(|e| e.key)
612
            .collect();
613
        let (mut trash, mut trash_version) = self.trash()?;
614
        let live: Vec<&String> = existing
615
            .iter()
616
            .filter(|k| !trash.iter().any(|(t, _)| t == *k))
617
            .collect();
618
        if live.len() > 1 {
619
            // Everything the record reaches, in dependency order, with
620
            // the state objects the bookmarks and tags name.
621
            let (bundle, changes) = self.with_mirror(|m| {
622
                let resp = m.remote_refs()?;
623
                let mut heads: Vec<ChangeHash> = Vec::new();
624
                for r in &resp.refs {
625
                    heads.extend(r.heads.iter().copied());
626
                }
627
                for t in &resp.tags {
628
                    heads.extend(t.heads.iter().copied());
629
                }
630
                heads.sort();
631
                heads.dedup();
632
                let changes = m.negotiate(&[], &heads)?;
633
                let mut bundle = m.bundle_for(&changes)?;
634
                for r in &resp.refs {
635
                    bundle.objects.push((
636
                        ObjectType::State,
637
                        StateObject::new(r.heads.clone()).encode(),
638
                    ));
639
                }
640
                for t in &resp.tags {
641
                    bundle.objects.push((
642
                        ObjectType::State,
643
                        StateObject::new(t.heads.clone()).encode(),
644
                    ));
645
                }
646
                Ok((bundle, changes.len()))
647
            })?;
648
            report.changes = changes;
649
            report.bundle_bytes = wire::encode_bundle(&bundle).len();
650
            let merged_key = self.upload(&bundle)?;
651
            let now = now_ms();
652
            for k in live {
653
                if *k != merged_key {
654
                    trash.push((k.clone(), now));
655
                    report.merged += 1;
656
                }
657
            }
658
            trash_version = Some(self.write_trash(&trash, trash_version.as_ref())?);
659
        }
660
        // What the grace period has released.
661
        let now = now_ms();
662
        let mut kept = Vec::new();
663
        for (k, when) in trash {
664
            if now.saturating_sub(when) >= grace.as_millis() as u64 {
665
                self.store.delete(&k)?;
666
                report.deleted += 1;
667
            } else {
668
                kept.push((k, when));
669
                report.deferred += 1;
9f9e72b9 Andy McCall ↶ 670
            }
d8d93678 Andy McCall ↶ 671
        }
672
        if report.deleted > 0 {
673
            self.write_trash(&kept, trash_version.as_ref())?;
9f9e72b9 Andy McCall ↶ 674
        }
d8d93678 Andy McCall ↶ 675
        let _ = id;
676
        Ok(report)
9f9e72b9 Andy McCall ↶ 677
    }
678
}
679
d8d93678 Andy McCall ↶ 680
impl Repo {
681
    /// `oxy gc --remote NAME`: a gc against a remote kept on a store.
682
    pub fn gc_remote(&self, name: &str, now: bool) -> Result<RemoteGc> {
683
        let Some(url) = self.config.remotes.get(name).cloned() else {
684
            return fail(format!("no remote {name:?}"));
685
        };
686
        let options = self.remote_options(name);
687
        let Some(store) = crate::protocol::open_store(&url, &options) else {
688
            return fail(format!(
689
                "gc --remote works on a remote kept on a store (s3://, gs://, az://, dir://); {name} is {url}"
690
            ));
691
        };
692
        let remote = BucketRemote::open(store?, &options)?;
693
        let grace = if now {
694
            std::time::Duration::ZERO
695
        } else {
696
            REMOTE_GC_GRACE
697
        };
698
        remote.gc(grace)
699
    }
700
}
701
9f9e72b9 Andy McCall ↶ 702
/// Open a remote on `store` for the local repository described by `options`.
703
pub fn open_store_remote(
704
    store: Box<dyn Store>,
705
    options: &RemoteOptions,
706
) -> Result<Box<dyn Remote>> {
707
    Ok(Box::new(BucketRemote::open(store, options)?))
708
}
709
710
/// Whether `path` holds a Strata store rather than a repository.
711
pub fn is_store_dir(path: &Path) -> bool {
712
    path.join(MARKER_KEY).is_file() || path.join(REFS_KEY).is_file()
713
}
714
715
#[cfg(test)]
716
mod tests {
717
    use super::*;
718
    use crate::repo::IdentityScope;
719
    use crate::strata::{Entry, LocalStore, MemoryStore};
720
    use std::ops::Range;
721
    use std::sync::Mutex;
722
723
    fn hermetic() {
724
        static ONCE: std::sync::Once = std::sync::Once::new();
725
        static DIR: std::sync::OnceLock<tempfile::TempDir> = std::sync::OnceLock::new();
726
        ONCE.call_once(|| {
727
            let d = tempfile::tempdir().unwrap();
728
            std::env::set_var("OXY_HOME", d.path());
729
            DIR.set(d).ok();
730
        });
731
    }
732
733
    fn write(root: &Path, rel: &str, content: &[u8]) {
734
        let p = root.join(rel);
735
        fs::create_dir_all(p.parent().unwrap()).unwrap();
736
        fs::write(p, content).unwrap();
737
    }
738
739
    fn add_remote(repo: &mut Repo, name: &str, url: &str) {
740
        repo.config.set(&format!("remote.{name}.url"), url).unwrap();
741
        repo.save_config().unwrap();
742
    }
743
744
    fn keys(store: &dyn Store, prefix: &str) -> Vec<String> {
745
        store
746
            .list(prefix)
747
            .unwrap()
748
            .into_iter()
749
            .map(|e| e.key)
750
            .collect()
751
    }
752
753
    #[test]
754
    fn push_clone_and_pull_through_a_directory_store() {
755
        hermetic();
756
        let dir = tempfile::tempdir().unwrap();
757
        let root = dir.path();
758
        let url = format!("dir://{}", root.join("bucket").display());
759
        let store = LocalStore::open(root.join("bucket")).unwrap();
760
761
        // A signs, commits, tags and pushes: the store gets the marker, one
762
        // bundle, and a record.
763
        let mut a = Repo::init(&root.join("a"))
764
            .unwrap()
765
            .with_home(root.join("home-a"));
766
        a.create_identity("Ada", "[email protected]", IdentityScope::Repo)
767
            .unwrap();
768
        add_remote(&mut a, "bucket", &url);
769
        write(&root.join("a"), "f.txt", b"a\n");
770
        let h1 = a.commit("first", &[]).unwrap();
771
        a.tag_create("v1", &[h1], "one").unwrap();
772
        let results = a.push("bucket", false).unwrap();
773
        assert!(results.iter().all(|r| r.ok), "{results:?}");
774
        assert_eq!(
775
            keys(&store, ""),
776
            [
777
                "bundles/00000001-".to_string() + &keys(&store, "bundles/")[0][17..],
778
                "oxygen.toml".into(),
779
                "refs".into()
780
            ]
781
        );
782
        assert_eq!(
783
            parse_marker(&store.get("oxygen.toml").unwrap(), "t").unwrap(),
784
            a.id().unwrap()
785
        );
786
        let again = a.push("bucket", false).unwrap();
787
        assert!(
788
            again[0].ok && again[0].reason.contains("up to date"),
789
            "{again:?}"
790
        );
791
        assert_eq!(
792
            keys(&store, "bundles/").len(),
793
            1,
794
            "a no-op push uploads nothing"
795
        );
796
797
        // B clones from the store: same id, same file, the tag, and the
798
        // seal on the change.
799
        let b = Repo::clone(&url, &root.join("b")).unwrap();
800
        assert_eq!(b.id().unwrap(), a.id().unwrap());
801
        assert_eq!(fs::read(root.join("b/f.txt")).unwrap(), b"a\n");
802
        assert_eq!(b.bookmark_heads().unwrap(), vec![h1]);
803
        assert_eq!(
804
            b.tags()
805
                .unwrap()
806
                .iter()
807
                .map(|t| t.name.clone())
808
                .collect::<Vec<_>>(),
809
            ["v1"]
810
        );
811
        assert!(matches!(
812
            b.authenticate_change(&h1).unwrap(),
813
            crate::repo::SignatureStatus::Valid { sealed: true, .. }
814
        ));
815
        assert!(
816
            root.join("b/.oxy/remotes/origin/strata/mirror/.oxy")
817
                .is_dir(),
818
            "the mirror lives with the remote's state"
819
        );
820
        assert!(
821
            !std::env::temp_dir()
822
                .read_dir()
823
                .unwrap()
824
                .flatten()
825
                .any(|e| e.file_name().to_string_lossy().starts_with("oxy-strata-")),
826
            "a clone's temporary mirror is removed"
827
        );
828
829
        // B commits and pushes; A pulls it. A then commits and pushes.
830
        write(&root.join("b"), "f.txt", b"a\nb\n");
831
        let h2 = b.commit("second", &[]).unwrap();
832
        assert!(b.push("origin", false).unwrap()[0].ok);
833
        assert_eq!(keys(&store, "bundles/").len(), 2);
834
        let pr = a.pull("bucket").unwrap();
835
        assert!(pr.changed && pr.checked_out);
836
        assert_eq!(fs::read(root.join("a/f.txt")).unwrap(), b"a\nb\n");
837
        write(&root.join("a"), "g.txt", b"g\n");
838
        let h3 = a.commit("third", &[]).unwrap();
839
        assert!(a.push("bucket", false).unwrap()[0].ok);
840
        let pr = b.pull("origin").unwrap();
841
        assert!(pr.changed);
842
        // g.txt is new, so its change commutes with B's: two heads.
843
        let mut want = vec![h2, h3];
844
        want.sort();
845
        assert_eq!(b.bookmark_heads().unwrap(), want);
846
        assert_eq!(fs::read(root.join("b/g.txt")).unwrap(), b"g\n");
847
848
        // A third clone gets everything from the three bundles.
849
        let c = Repo::clone(&url, &root.join("c")).unwrap();
850
        assert_eq!(c.bookmark_heads().unwrap(), want);
851
        assert_eq!(fs::read(root.join("c/f.txt")).unwrap(), b"a\nb\n");
852
    }
853
854
    #[test]
855
    fn a_push_behind_the_remote_is_refused_before_anything_is_uploaded() {
856
        hermetic();
857
        let dir = tempfile::tempdir().unwrap();
858
        let root = dir.path();
859
        let url = format!("dir://{}", root.join("bucket").display());
860
        let store = LocalStore::open(root.join("bucket")).unwrap();
861
        let mut a = Repo::init(&root.join("a")).unwrap();
862
        add_remote(&mut a, "bucket", &url);
863
        write(&root.join("a"), "f.txt", b"a\n");
864
        a.commit("first", &[]).unwrap();
865
        assert!(a.push("bucket", false).unwrap()[0].ok);
866
        let b = Repo::clone(&url, &root.join("b")).unwrap();
867
        write(&root.join("b"), "f.txt", b"b\n");
868
        b.commit("from b", &[]).unwrap();
869
        assert!(b.push("origin", false).unwrap()[0].ok);
870
        write(&root.join("a"), "g.txt", b"g\n");
871
        a.commit("from a", &[]).unwrap();
872
        let e = a.push("bucket", false).unwrap_err().to_string();
873
        assert!(e.contains("pull first"), "{e}");
874
        assert_eq!(keys(&store, "bundles/").len(), 2);
875
        assert!(a.pull("bucket").unwrap().changed);
876
        assert!(a.push("bucket", false).unwrap()[0].ok);
877
        assert_eq!(keys(&store, "bundles/").len(), 3);
878
    }
879
880
    /// A store whose record moves under a writer the first time it is
881
    /// written to: what a concurrent push looks like between the read and
882
    /// the compare-and-swap.
883
    struct Racing {
884
        inner: MemoryStore,
885
        raced: Mutex<bool>,
886
    }
887
888
    impl Store for Racing {
889
        fn describe(&self) -> String {
890
            "racing".into()
891
        }
892
        fn has(&self, key: &str) -> crate::strata::Result<bool> {
893
            self.inner.has(key)
894
        }
895
        fn get(&self, key: &str) -> crate::strata::Result<Vec<u8>> {
896
            self.inner.get(key)
897
        }
898
        fn get_range(&self, key: &str, range: Range<u64>) -> crate::strata::Result<Vec<u8>> {
899
            self.inner.get_range(key, range)
900
        }
901
        fn put_new(&self, key: &str, bytes: &[u8]) -> crate::strata::Result<()> {
902
            self.inner.put_new(key, bytes)
903
        }
904
        fn list(&self, prefix: &str) -> crate::strata::Result<Vec<Entry>> {
905
            self.inner.list(prefix)
906
        }
907
        fn delete(&self, key: &str) -> crate::strata::Result<()> {
908
            self.inner.delete(key)
909
        }
910
        fn read_ref(&self, key: &str) -> crate::strata::Result<Option<(Vec<u8>, Version)>> {
911
            self.inner.read_ref(key)
912
        }
913
        fn write_ref(
914
            &self,
915
            key: &str,
916
            bytes: &[u8],
917
            expected: Option<&Version>,
918
        ) -> crate::strata::Result<Version> {
919
            let mut raced = self.raced.lock().unwrap();
920
            if !*raced && expected.is_some() {
921
                *raced = true;
922
                // Someone else lands a record: the same references with a
923
                // different detail, so the version moves.
924
                let (current, v) = self.inner.read_ref(key)?.unwrap();
925
                let (id, mut resp) =
926
                    wire::decode_refs(&current).map_err(|e| StoreError::Backend(e.to_string()))?;
927
                resp.default_bookmark = Some("raced".into());
928
                self.inner
929
                    .write_ref(key, &wire::encode_refs(id, &resp), Some(&v))?;
930
            }
931
            self.inner.write_ref(key, bytes, expected)
932
        }
933
    }
934
935
    #[test]
d8d93678 Andy McCall ↶ 936
    fn a_record_moved_during_a_push_is_applied_over_what_landed() {
9f9e72b9 Andy McCall ↶ 937
        hermetic();
938
        let dir = tempfile::tempdir().unwrap();
939
        let root = dir.path();
940
        let a = Repo::init(&root.join("a")).unwrap();
941
        write(&root.join("a"), "f.txt", b"a\n");
942
        let h1 = a.commit("first", &[]).unwrap();
943
        let store = Box::new(Racing {
944
            inner: MemoryStore::new(),
945
            raced: Mutex::new(false),
946
        });
947
        let options = RemoteOptions {
948
            repo_id: Some(a.id().unwrap()),
949
            cache_dir: Some(root.join("cache")),
950
            ..Default::default()
951
        };
952
        let remote = BucketRemote::open(store, &options).unwrap();
953
        // The first push creates the record (expected None: no race yet).
954
        let state = a.store.put_state(&StateObject::new(vec![h1])).unwrap();
955
        let empty = a.store.put_state(&StateObject::new(vec![])).unwrap();
956
        let mut bundle = a.bundle_for(&[h1]).unwrap();
957
        bundle
958
            .objects
959
            .push((ObjectType::State, StateObject::new(vec![h1]).encode()));
960
        let update = |old, new| RefUpdate {
961
            name: "main".into(),
962
            old_state: old,
963
            new_state: new,
964
            force: false,
965
            attestation: None,
966
        };
967
        let r = remote
968
            .push(bundle, vec![update(empty, state)], None, vec![])
969
            .unwrap();
970
        assert!(r[0].ok, "{r:?}");
971
        // The second push meets the race: refused, nothing moved.
972
        write(&root.join("a"), "f.txt", b"ab\n");
973
        let h2 = a.commit("second", &[]).unwrap();
974
        let state2 = a.store.put_state(&StateObject::new(vec![h2])).unwrap();
975
        let mut bundle = a.bundle_for(&[h2]).unwrap();
976
        bundle
977
            .objects
978
            .push((ObjectType::State, StateObject::new(vec![h2]).encode()));
979
        let r = remote
980
            .push(bundle, vec![update(state, state2)], None, vec![])
981
            .unwrap();
d8d93678 Andy McCall ↶ 982
        assert!(r[0].ok, "applied over what landed meanwhile: {r:?}");
9f9e72b9 Andy McCall ↶ 983
        let refs = remote.refs().unwrap();
984
        assert_eq!(
985
            refs.refs[0].heads,
d8d93678 Andy McCall ↶ 986
            vec![h2],
987
            "the store and the mirror both moved"
9f9e72b9 Andy McCall ↶ 988
        );
989
    }
990
991
    #[test]
992
    fn a_store_holds_one_repository() {
993
        hermetic();
994
        let dir = tempfile::tempdir().unwrap();
995
        let root = dir.path();
996
        let url = format!("dir://{}", root.join("bucket").display());
997
        let mut a = Repo::init(&root.join("a")).unwrap();
998
        add_remote(&mut a, "bucket", &url);
999
        write(&root.join("a"), "f.txt", b"a\n");
1000
        a.commit("first", &[]).unwrap();
1001
        assert!(a.push("bucket", false).unwrap()[0].ok);
1002
        let mut other = Repo::init(&root.join("other")).unwrap();
1003
        add_remote(&mut other, "bucket", &url);
1004
        write(&root.join("other"), "f.txt", b"o\n");
1005
        other.commit("mine", &[]).unwrap();
1006
        let e = other.push("bucket", false).unwrap_err().to_string();
1007
        assert!(
1008
            e.contains("different repository") || e.contains("another repository"),
1009
            "{e}"
1010
        );
1011
        assert_eq!(
1012
            keys(&LocalStore::open(root.join("bucket")).unwrap(), "bundles/").len(),
1013
            1
1014
        );
1015
    }
1016
1017
    #[test]
1018
    fn a_damaged_bundle_is_caught_and_an_empty_store_cannot_be_cloned() {
1019
        hermetic();
1020
        let dir = tempfile::tempdir().unwrap();
1021
        let root = dir.path();
1022
        let url = format!("dir://{}", root.join("bucket").display());
1023
        let e = Repo::clone(&url, &root.join("nothing"))
1024
            .err()
1025
            .expect("refused")
1026
            .to_string();
1027
        assert!(e.contains("empty and unbound"), "{e}");
1028
        let mut a = Repo::init(&root.join("a")).unwrap();
1029
        add_remote(&mut a, "bucket", &url);
1030
        write(&root.join("a"), "f.txt", b"a\n");
1031
        a.commit("first", &[]).unwrap();
1032
        assert!(a.push("bucket", false).unwrap()[0].ok);
1033
        let store = LocalStore::open(root.join("bucket")).unwrap();
1034
        let key = keys(&store, "bundles/").remove(0);
1035
        let mut bytes = store.get(&key).unwrap();
1036
        let mid = bytes.len() / 2;
1037
        bytes[mid] ^= 0xff;
1038
        store.delete(&key).unwrap();
1039
        store.put_new(&key, &bytes).unwrap();
1040
        let e = Repo::clone(&url, &root.join("b"))
1041
            .err()
1042
            .expect("refused")
1043
            .to_string();
1044
        assert!(
1045
            e.contains(&key) || e.contains("bundle") || e.contains("hash"),
1046
            "{e}"
1047
        );
1048
        assert!(!root.join("b/f.txt").exists());
d8d93678 Andy McCall ↶ 1049
    }
1050
1051
    /// A store shared by two remotes in one test, with a hook run once at
1052
    /// the first conditional write: what another push landing between a
1053
    /// read of the record and its compare-and-swap looks like.
1054
    struct Shared {
1055
        inner: std::sync::Arc<MemoryStore>,
1056
        fail_uploads: std::sync::atomic::AtomicBool,
1057
        vanish: Mutex<Option<String>>,
1058
    }
1059
1060
    thread_local! {
1061
        static HOOK: std::cell::RefCell<Option<Box<dyn FnOnce()>>> = const { std::cell::RefCell::new(None) };
1062
    }
1063
1064
    impl Shared {
1065
        fn new(inner: std::sync::Arc<MemoryStore>) -> Box<Shared> {
1066
            Box::new(Shared {
1067
                inner,
1068
                fail_uploads: Default::default(),
1069
                vanish: Mutex::new(None),
1070
            })
1071
        }
1072
    }
1073
1074
    impl Store for Shared {
1075
        fn describe(&self) -> String {
1076
            "shared".into()
1077
        }
1078
        fn has(&self, key: &str) -> crate::strata::Result<bool> {
1079
            self.inner.has(key)
1080
        }
1081
        fn get(&self, key: &str) -> crate::strata::Result<Vec<u8>> {
1082
            let mut vanish = self.vanish.lock().unwrap();
1083
            if vanish.as_deref() == Some(key) {
1084
                *vanish = None;
1085
                return Err(StoreError::NotFound(key.to_string()));
1086
            }
1087
            self.inner.get(key)
1088
        }
1089
        fn get_range(&self, key: &str, range: Range<u64>) -> crate::strata::Result<Vec<u8>> {
1090
            self.inner.get_range(key, range)
1091
        }
1092
        fn put_new(&self, key: &str, bytes: &[u8]) -> crate::strata::Result<()> {
1093
            if key.starts_with("bundles/")
1094
                && self.fail_uploads.load(std::sync::atomic::Ordering::SeqCst)
1095
            {
1096
                return Err(StoreError::Backend("shared: the disk is full".into()));
1097
            }
1098
            self.inner.put_new(key, bytes)
1099
        }
1100
        fn list(&self, prefix: &str) -> crate::strata::Result<Vec<Entry>> {
1101
            self.inner.list(prefix)
1102
        }
1103
        fn delete(&self, key: &str) -> crate::strata::Result<()> {
1104
            self.inner.delete(key)
1105
        }
1106
        fn read_ref(&self, key: &str) -> crate::strata::Result<Option<(Vec<u8>, Version)>> {
1107
            self.inner.read_ref(key)
1108
        }
1109
        fn write_ref(
1110
            &self,
1111
            key: &str,
1112
            bytes: &[u8],
1113
            expected: Option<&Version>,
1114
        ) -> crate::strata::Result<Version> {
1115
            if key == "refs" && expected.is_some() {
1116
                if let Some(hook) = HOOK.with(|h| h.borrow_mut().take()) {
1117
                    hook();
1118
                }
1119
            }
1120
            self.inner.write_ref(key, bytes, expected)
1121
        }
1122
    }
1123
1124
    fn repo_with_commit(root: &Path, name: &str, file: &str, text: &[u8], msg: &str) -> Repo {
1125
        let repo = Repo::init(&root.join(name)).unwrap();
1126
        write(&root.join(name), file, text);
1127
        repo.commit(msg, &[]).unwrap();
1128
        repo
1129
    }
1130
1131
    fn remote_for(repo: &Repo, store: Box<dyn Store>, root: &Path, name: &str) -> BucketRemote {
1132
        let options = RemoteOptions {
1133
            repo_id: Some(repo.id().unwrap()),
1134
            cache_dir: Some(root.join(format!("cache-{name}"))),
1135
            ..Default::default()
1136
        };
1137
        BucketRemote::open(store, &options).unwrap()
1138
    }
1139
1140
    fn push_bookmark(
1141
        repo: &Repo,
1142
        remote: &BucketRemote,
1143
        bookmark: &str,
1144
        old: Option<ObjectRef>,
1145
    ) -> Vec<RefResult> {
1146
        let heads = repo.heads_of(&repo.view().unwrap(), bookmark).unwrap();
1147
        let state = repo
1148
            .store
1149
            .put_state(&StateObject::new(heads.clone()))
1150
            .unwrap();
1151
        let empty = repo.store.put_state(&StateObject::new(vec![])).unwrap();
1152
        // Only what the remote lacks, as push_bookmark sends.
1153
        let have: Vec<ChangeHash> = remote
1154
            .refs()
1155
            .unwrap()
1156
            .refs
1157
            .iter()
1158
            .flat_map(|r| r.heads.clone())
1159
            .collect();
1160
        let changes = repo.negotiate(&have, &heads).unwrap();
1161
        let mut bundle = repo.bundle_for(&changes).unwrap();
1162
        bundle
1163
            .objects
1164
            .push((ObjectType::State, StateObject::new(heads).encode()));
1165
        let update = RefUpdate {
1166
            name: bookmark.into(),
1167
            old_state: old.unwrap_or(empty),
1168
            new_state: state,
1169
            force: false,
1170
            attestation: None,
1171
        };
1172
        remote.push(bundle, vec![update], None, vec![]).unwrap()
1173
    }
1174
1175
    #[test]
1176
    fn two_pushes_to_different_bookmarks_both_land() {
1177
        hermetic();
1178
        let dir = tempfile::tempdir().unwrap();
1179
        let root = dir.path();
1180
        let inner = std::sync::Arc::new(MemoryStore::new());
1181
        // A pushes main; B (a clone of the same repository) pushes a
1182
        // second bookmark between A's read and A's compare-and-swap.
1183
        let a = repo_with_commit(root, "a", "f.txt", b"a\n", "first");
1184
        let ra = remote_for(&a, Shared::new(inner.clone()), root, "a");
1185
        let r = push_bookmark(&a, &ra, "main", None);
1186
        assert!(r[0].ok, "{r:?}");
1187
        let b = Repo::init_with_id(&root.join("b"), a.id().unwrap()).unwrap();
1188
        let rb = remote_for(&b, Shared::new(inner.clone()), root, "b");
1189
        rb.refs().unwrap();
1190
        write(&root.join("b"), "g.txt", b"g\n");
1191
        b.commit("on b", &[]).unwrap();
1192
        let (_, op) = b.head_op().unwrap();
1193
        let mut view = op.view.clone();
1194
        let feature_heads = b.heads_of(&view, "main").unwrap();
1195
        view.bookmarks.insert(
1196
            "feature".into(),
1197
            b.store.put_state(&StateObject::new(feature_heads)).unwrap(),
1198
        );
1199
        b.record_op(&[b.head_op().unwrap().0], "bookmark create feature", view)
1200
            .unwrap();
1201
        // A's second push to main; B's push of feature lands in between.
1202
        write(&root.join("a"), "f.txt", b"aa\n");
1203
        a.commit("second", &[]).unwrap();
1204
        let a_old = ra
1205
            .refs()
1206
            .unwrap()
1207
            .refs
1208
            .iter()
1209
            .find(|r| r.name == "main")
1210
            .unwrap()
1211
            .state;
1212
        let b_remote = std::sync::Arc::new(std::sync::Mutex::new(Some(rb)));
1213
        let b_handle = b_remote.clone();
1214
        HOOK.with(|h| {
1215
            *h.borrow_mut() = Some(Box::new(move || {
1216
                let rb = b_handle.lock().unwrap().take().unwrap();
1217
                let r = push_bookmark(&b, &rb, "feature", None);
1218
                assert!(r[0].ok, "{r:?}");
1219
            }))
1220
        });
1221
        let r = push_bookmark(&a, &ra, "main", Some(a_old));
1222
        assert!(r[0].ok, "a's push is applied over b's: {r:?}");
1223
        let refs = ra.refs().unwrap();
1224
        let names: Vec<&str> = refs.refs.iter().map(|r| r.name.as_str()).collect();
1225
        assert_eq!(names, ["feature", "main"], "both landed");
1226
        assert_eq!(inner.list("bundles/").unwrap().len(), 3);
1227
        let _ = b_remote;
1228
    }
1229
1230
    #[test]
1231
    fn an_interrupted_push_leaves_the_store_usable() {
1232
        hermetic();
1233
        let dir = tempfile::tempdir().unwrap();
1234
        let root = dir.path();
1235
        let inner = std::sync::Arc::new(MemoryStore::new());
1236
        let a = repo_with_commit(root, "a", "f.txt", b"a\n", "first");
1237
        let shared = Shared::new(inner.clone());
1238
        let fail = &shared.fail_uploads as *const std::sync::atomic::AtomicBool;
1239
        let ra = remote_for(&a, shared, root, "a");
1240
        assert!(push_bookmark(&a, &ra, "main", None)[0].ok);
1241
        let old = ra.refs().unwrap().refs[0].state;
1242
        // The upload fails: nothing in the store changes, the mirror is
1243
        // honest, and the next push gets through.
1244
        write(&root.join("a"), "f.txt", b"aa\n");
1245
        let h2 = a.commit("second", &[]).unwrap();
1246
        unsafe { (*fail).store(true, std::sync::atomic::Ordering::SeqCst) };
1247
        let e = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1248
            push_bookmark(&a, &ra, "main", Some(old))
1249
        }));
1250
        assert!(e.is_err(), "the push reports the failed upload");
1251
        assert_eq!(inner.list("bundles/").unwrap().len(), 1);
1252
        let refs = ra.refs().unwrap();
1253
        assert_eq!(refs.refs[0].state, old, "the record did not move");
1254
        unsafe { (*fail).store(false, std::sync::atomic::Ordering::SeqCst) };
1255
        let r = push_bookmark(&a, &ra, "main", Some(old));
1256
        assert!(r[0].ok, "{r:?}");
1257
        assert_eq!(ra.refs().unwrap().refs[0].heads, vec![h2]);
1258
    }
1259
1260
    #[test]
1261
    fn bundles_out_of_order_or_gone_are_handled() {
1262
        hermetic();
1263
        let dir = tempfile::tempdir().unwrap();
1264
        let root = dir.path();
1265
        let inner = std::sync::Arc::new(MemoryStore::new());
1266
        let a = repo_with_commit(root, "a", "f.txt", b"a\n", "first");
1267
        let ra = remote_for(&a, Shared::new(inner.clone()), root, "a");
1268
        assert!(push_bookmark(&a, &ra, "main", None)[0].ok);
1269
        let old = ra.refs().unwrap().refs[0].state;
1270
        write(&root.join("a"), "f.txt", b"aa\n");
1271
        let h2 = a.commit("second", &[]).unwrap();
1272
        assert!(push_bookmark(&a, &ra, "main", Some(old))[0].ok);
1273
        // The second bundle (which needs the first) renamed to sort first.
1274
        let keys: Vec<String> = inner
1275
            .list("bundles/")
1276
            .unwrap()
1277
            .into_iter()
1278
            .map(|e| e.key)
1279
            .collect();
1280
        assert_eq!(keys.len(), 2);
1281
        let bytes = inner.get(&keys[1]).unwrap();
1282
        inner.delete(&keys[1]).unwrap();
1283
        inner
1284
            .put_new("bundles/00000000-first.bundle", &bytes)
1285
            .unwrap();
1286
        let shared = Shared::new(inner.clone());
1287
        let c = Repo::init_with_id(&root.join("c"), a.id().unwrap()).unwrap();
1288
        let rc = remote_for(&c, shared, root, "c");
1289
        assert_eq!(
1290
            rc.refs().unwrap().refs[0].heads,
1291
            vec![h2],
1292
            "the dependent bundle waited for its dependency"
1293
        );
1294
        // A bundle gone between the listing and the read is skipped.
1295
        let shared = Shared::new(inner.clone());
1296
        *shared.vanish.lock().unwrap() = Some(keys[0].clone());
1297
        let d = Repo::init_with_id(&root.join("d"), a.id().unwrap()).unwrap();
1298
        let rd = remote_for(&d, shared, root, "d");
1299
        // Without the first bundle the second cannot be imported: the sync
1300
        // says so rather than failing silently.
1301
        let e = rd.refs().expect_err("missing changes").to_string();
1302
        assert!(e.contains("cannot be imported"), "{e}");
1303
    }
1304
1305
    #[test]
1306
    fn gc_merges_bundles_and_prunes_after_the_grace_period() {
1307
        hermetic();
1308
        let dir = tempfile::tempdir().unwrap();
1309
        let root = dir.path();
1310
        let url = format!("dir://{}", root.join("bucket").display());
1311
        let store = LocalStore::open(root.join("bucket")).unwrap();
1312
        let mut a = Repo::init(&root.join("a")).unwrap();
1313
        add_remote(&mut a, "bucket", &url);
1314
        for i in 1..=3 {
1315
            write(&root.join("a"), "f.txt", format!("{i}\n").as_bytes());
1316
            a.commit(&format!("change {i}"), &[]).unwrap();
1317
            assert!(a.push("bucket", false).unwrap()[0].ok);
1318
        }
1319
        a.tag_create("v3", &a.bookmark_heads().unwrap(), "three")
1320
            .unwrap();
1321
        assert!(a.push("bucket", false).unwrap().iter().all(|r| r.ok));
1322
        assert_eq!(
1323
            keys(&store, "bundles/").len(),
1324
            4,
1325
            "three pushes and the tag's"
1326
        );
1327
        // A gc folds the four into one; the old ones wait out the grace.
1328
        let r = a.gc_remote("bucket", false).unwrap();
1329
        assert_eq!(
1330
            (r.merged, r.changes, r.deleted, r.deferred),
1331
            (4, 3, 0, 4),
1332
            "{r:?}"
1333
        );
1334
        assert!(r.bundle_bytes > 0);
1335
        assert_eq!(keys(&store, "bundles/").len(), 5);
1336
        let b = Repo::clone(&url, &root.join("b")).unwrap();
1337
        assert_eq!(fs::read(root.join("b/f.txt")).unwrap(), b"3\n");
1338
        assert_eq!(b.tags().unwrap().len(), 1);
1339
        // Past the grace period they go, and a clone needs only the one.
1340
        let r = a.gc_remote("bucket", true).unwrap();
1341
        assert_eq!((r.merged, r.deleted, r.deferred), (0, 4, 0), "{r:?}");
1342
        assert_eq!(keys(&store, "bundles/").len(), 1);
1343
        let c = Repo::clone(&url, &root.join("c")).unwrap();
1344
        assert_eq!(c.bookmark_heads().unwrap(), a.bookmark_heads().unwrap());
1345
        // The earlier clone, which saw the old bundles, carries on.
1346
        write(&root.join("b"), "f.txt", b"4\n");
1347
        b.commit("four", &[]).unwrap();
1348
        assert!(b.push("origin", false).unwrap()[0].ok);
1349
        assert!(a.pull("bucket").unwrap().changed);
1350
        assert_eq!(fs::read(root.join("a/f.txt")).unwrap(), b"4\n");
1351
        // Nothing more to merge: a second gc is a no-op.
1352
        let r = a.gc_remote("bucket", true).unwrap();
1353
        assert_eq!((r.merged, r.deleted), (2, 2));
1354
        assert!(a.gc_remote("nope", true).is_err());
9f9e72b9 Andy McCall ↶ 1355
    }
1356
1357
    #[test]
1358
    fn markers_parse_and_refuse_newer_formats() {
1359
        let id = [7u8; 16];
1360
        assert_eq!(parse_marker(&marker(id), "s").unwrap(), id);
1361
        let e = parse_marker(
1362
            b"format = 9\nrepo = \"07070707070707070707070707070707\"\n",
1363
            "s",
1364
        )
1365
        .unwrap_err()
1366
        .to_string();
1367
        assert!(e.contains("format 9"), "{e}");
1368
        assert!(parse_marker(b"hello", "s").is_err());
1369
    }
1370
}