This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

knot2/xrpc: forks take upstream's object format

Lewis: May this revision serve well! <did:plc:3fwecdnvtcscjnrx2p4n7alz>

author did:plc:3fwecdnvtcscjnrx2p4n7a… date (Jul 27, 2026, 7:45 PM +0300) commit 6b5b544c parent e4840a76 change-id slvzzksx
+269 -81
+11 -7
knot2/crates/knot-git/src/repo.rs
··· 212 212 } 213 213 214 214 pub fn create(&self, did: &RepoDid) -> Result<Repo, GitError> { 215 - self.init_repo(self.guarded_path(did)?) 215 + self.create_with_format(did, self.object_format) 216 + } 217 + 218 + pub fn create_with_format( 219 + &self, 220 + did: &RepoDid, 221 + format: ObjectFormat, 222 + ) -> Result<Repo, GitError> { 223 + let repo = Repo::create_with_format(self.guarded_path(did)?, format)?; 224 + repo.set_head(&self.head)?; 225 + Ok(repo) 216 226 } 217 227 218 228 pub fn remove(&self, did: &RepoDid) -> Result<(), GitError> { ··· 232 242 233 243 pub fn bootstrap_meta(&self, knot: &KnotId) -> Result<Repo, GitError> { 234 244 init_bare_idempotent(self.meta_path(knot)?) 235 - } 236 - 237 - fn init_repo(&self, path: PathBuf) -> Result<Repo, GitError> { 238 - let repo = Repo::create_with_format(path, self.object_format)?; 239 - repo.set_head(&self.head)?; 240 - Ok(repo) 241 245 } 242 246 } 243 247
+74 -12
knot2/crates/knot-pack/src/fetch.rs
··· 3 3 use axum::http::{HeaderMap, HeaderValue, Method, header}; 4 4 use knot_git::{Filter, RefRecord, Repo}; 5 5 use knot_runtime::{HttpRequest, HttpResponse, HttpTransport, NetworkError}; 6 - use knot_types::{HttpStatus, Oid, RefName}; 6 + use knot_types::{HttpStatus, ObjectFormat, Oid, RefName}; 7 7 use url::Url; 8 8 9 9 use crate::error::PackError; ··· 30 30 31 31 #[derive(Debug, Clone, PartialEq, Eq)] 32 32 pub struct UpstreamRefs { 33 + pub object_format: ObjectFormat, 33 34 pub head_symref: Option<RefName>, 34 35 pub refs: Vec<RefRecord>, 35 36 } ··· 81 82 Ok(response) 82 83 } 83 84 84 - pub fn parse_advertisement(body: &[u8]) -> Result<(), FetchError> { 85 + pub fn parse_advertisement(body: &[u8]) -> Result<ObjectFormat, FetchError> { 85 86 let lines = pkt::data_payloads_all(body).map_err(|error| protocol(error.to_string()))?; 86 87 let lines: Vec<&str> = lines 87 88 .iter() ··· 98 99 if !has("ls-refs") || !has("fetch") { 99 100 return Err(protocol("upstream is missing ls-refs or fetch v2 command")); 100 101 } 101 - Ok(()) 102 + lines 103 + .iter() 104 + .find_map(|line| line.strip_prefix("object-format=")) 105 + .map_or(Ok(ObjectFormat::SHA1), |token| { 106 + ObjectFormat::from_capability(token).ok_or_else(|| { 107 + protocol(format!("upstream advertises unknown object format {token}")) 108 + }) 109 + }) 102 110 } 103 111 104 112 pub fn ls_refs_request(prefixes: &[&str]) -> Result<Vec<u8>, PackError> { ··· 114 122 Ok(buf) 115 123 } 116 124 117 - pub fn parse_ls_refs(body: &[u8]) -> Result<UpstreamRefs, FetchError> { 125 + pub fn parse_ls_refs(body: &[u8], object_format: ObjectFormat) -> Result<UpstreamRefs, FetchError> { 118 126 let lines = pkt::data_payloads(body).map_err(|error| protocol(error.to_string()))?; 119 127 lines.iter().try_fold( 120 128 UpstreamRefs { 129 + object_format, 121 130 head_symref: None, 122 131 refs: Vec::new(), 123 132 }, ··· 133 142 .ok_or_else(|| protocol(format!("malformed ref line: {text}")))?; 134 143 let target = 135 144 Oid::from_hex(oid).map_err(|_| protocol(format!("malformed ref oid: {oid}")))?; 145 + if target.object_id().kind() != object_format.kind() { 146 + return Err(protocol(format!( 147 + "ref oid {oid} doesn't match advertised object format {}", 148 + object_format.capability() 149 + ))); 150 + } 136 151 let mut attributes = rest.split(' '); 137 152 match attributes.next() { 138 153 Some("HEAD") => { ··· 228 243 }, 229 244 ) 230 245 .await?; 231 - parse_advertisement(&response.body)?; 246 + let object_format = parse_advertisement(&response.body)?; 232 247 233 248 let upload = endpoint(base, "git-upload-pack")?; 234 249 let response = execute( ··· 241 256 }, 242 257 ) 243 258 .await?; 244 - parse_ls_refs(&response.body) 259 + parse_ls_refs(&response.body, object_format) 245 260 } 246 261 247 262 pub async fn remote_pack( ··· 277 292 .cloned() 278 293 .collect(); 279 294 Ok(UpstreamRefs { 295 + object_format: source.object_format(), 280 296 head_symref: source.head().map(|head| head.name), 281 297 refs, 282 298 }) ··· 350 366 } 351 367 352 368 #[test] 353 - fn the_v2_advertisement_is_accepted_and_v0_is_refused() { 369 + fn the_v2_advertisement_parses_to_its_object_format_and_v0_is_refused() { 354 370 let scan = tempfile::tempdir().unwrap(); 355 371 let repo = knot_git::Layout::new(scan.path()) 356 372 .create(&knot_types::RepoDid::new("did:plc:squid").unwrap()) 357 373 .unwrap(); 358 374 let v2 = crate::upload::advertise(&repo).unwrap(); 359 - assert!(parse_advertisement(&v2).is_ok()); 375 + assert_eq!(parse_advertisement(&v2).unwrap(), ObjectFormat::SHA1); 376 + 377 + let advertise = |extra: Option<&str>| { 378 + let mut buf = Vec::new(); 379 + data(&mut buf, b"version 2\n"); 380 + data(&mut buf, b"ls-refs\n"); 381 + data(&mut buf, b"fetch\n"); 382 + if let Some(extra) = extra { 383 + data(&mut buf, format!("{extra}\n").as_bytes()); 384 + } 385 + pkt::write_flush(&mut buf).unwrap(); 386 + parse_advertisement(&buf) 387 + }; 388 + assert_eq!( 389 + advertise(Some("object-format=sha256")).unwrap(), 390 + ObjectFormat::SHA256 391 + ); 392 + assert_eq!( 393 + advertise(None).unwrap(), 394 + ObjectFormat::SHA1, 395 + "protocol v2 treats an absent object-format as sha1" 396 + ); 397 + assert!(matches!( 398 + advertise(Some("object-format=sha3")), 399 + Err(FetchError::Protocol(_)) 400 + )); 360 401 361 402 let mut v0 = Vec::new(); 362 403 data(&mut v0, b"# service=git-upload-pack\n"); ··· 373 414 } 374 415 375 416 #[test] 376 - fn ls_refs_lines_parse_with_symref_and_skip_head() { 417 + fn ls_refs_lines_parse_with_symref_skip_head_and_enforce_the_object_format() { 377 418 let mut body = Vec::new(); 378 419 data( 379 420 &mut body, ··· 384 425 b"95d09f2b10159347eece71399a7e2e907ea3df4f refs/heads/main\n", 385 426 ); 386 427 pkt::write_flush(&mut body).unwrap(); 387 - let refs = parse_ls_refs(&body).unwrap(); 428 + let refs = parse_ls_refs(&body, ObjectFormat::SHA1).unwrap(); 388 429 assert_eq!( 389 430 refs.head_symref.as_ref().map(RefName::as_str), 390 431 Some("refs/heads/main") ··· 392 433 assert_eq!(refs.refs.len(), 1); 393 434 assert_eq!(refs.refs[0].name.as_str(), "refs/heads/main"); 394 435 assert_eq!(refs.tips().len(), 1); 436 + 437 + let single = |oid: &str| { 438 + let mut buf = Vec::new(); 439 + data(&mut buf, format!("{oid} refs/heads/main\n").as_bytes()); 440 + pkt::write_flush(&mut buf).unwrap(); 441 + buf 442 + }; 443 + let sha1 = "95d09f2b10159347eece71399a7e2e907ea3df4f"; 444 + let sha256 = "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321"; 445 + assert!(parse_ls_refs(&single(sha256), ObjectFormat::SHA256).is_ok()); 446 + assert!(matches!( 447 + parse_ls_refs(&single(sha256), ObjectFormat::SHA1), 448 + Err(FetchError::Protocol(_)) 449 + )); 450 + assert!(matches!( 451 + parse_ls_refs(&single(sha1), ObjectFormat::SHA256), 452 + Err(FetchError::Protocol(_)) 453 + )); 395 454 } 396 455 397 456 #[test] ··· 399 458 let mut body = Vec::new(); 400 459 data(&mut body, b"zzzz refs/heads/main\n"); 401 460 pkt::write_flush(&mut body).unwrap(); 402 - assert!(matches!(parse_ls_refs(&body), Err(FetchError::Protocol(_)))); 461 + assert!(matches!( 462 + parse_ls_refs(&body, ObjectFormat::SHA1), 463 + Err(FetchError::Protocol(_)) 464 + )); 403 465 } 404 466 405 467 #[test] ··· 408 470 data(&mut body, b"ERR access denied\n"); 409 471 pkt::write_flush(&mut body).unwrap(); 410 472 assert!(matches!( 411 - parse_ls_refs(&body), 473 + parse_ls_refs(&body, ObjectFormat::SHA1), 412 474 Err(FetchError::Remote(message)) if message == "access denied" 413 475 )); 414 476 }
+39 -2
knot2/crates/knot-xrpc/src/forks.rs
··· 14 14 use knot_pack::{FetchError, HaveOids, PackLimits, UpstreamRefs, WantOids}; 15 15 use knot_postreceive::{Actor, Ci}; 16 16 use knot_runtime::{Clock, HttpTransport}; 17 - use knot_types::{BranchName, Oid, OwnerDid, RefName, RepoDid}; 17 + use knot_types::{BranchName, ObjectFormat, Oid, OwnerDid, RefName, RepoDid}; 18 18 19 19 use crate::body::{ForkRef, RemoteRef, RepoAtUri, RepoNameArg, Revspec, SourceUrl}; 20 20 use crate::branches::resolve_at_uri; ··· 82 82 return resolve_local_path(state, url).map(Upstream::Local); 83 83 } 84 84 Ok(Upstream::Remote(url.clone())) 85 + } 86 + 87 + pub(crate) struct ForkSource { 88 + pub(crate) origin: SourceUrl, 89 + pub(crate) upstream: Upstream, 90 + pub(crate) refs: UpstreamRefs, 91 + } 92 + 93 + impl ForkSource { 94 + pub(crate) async fn resolve<H: HttpTransport, C: Clock>( 95 + state: &XrpcState<H, C>, 96 + origin: &SourceUrl, 97 + ) -> Result<Self, XrpcError> { 98 + let upstream = resolve_upstream(state, origin)?; 99 + let prefixes = ["HEAD", "refs/heads/", "refs/tags/"] 100 + .iter() 101 + .map(|prefix| prefix.to_string()) 102 + .collect(); 103 + let refs = upstream_refs(state, &upstream, prefixes).await?; 104 + Ok(Self { 105 + origin: origin.clone(), 106 + upstream, 107 + refs, 108 + }) 109 + } 85 110 } 86 111 87 112 fn map_fetch(error: FetchError) -> XrpcError { ··· 272 297 struct ForkState { 273 298 origin: SourceUrl, 274 299 haves: Vec<Oid>, 300 + object_format: ObjectFormat, 275 301 } 276 302 277 303 fn load_fork_state(repo: &Repo) -> Result<ForkState, XrpcError> { ··· 285 311 .into_iter() 286 312 .map(|record| record.target) 287 313 .collect(); 288 - Ok(ForkState { origin, haves }) 314 + Ok(ForkState { 315 + origin, 316 + haves, 317 + object_format: repo.object_format(), 318 + }) 289 319 } 290 320 291 321 pub(crate) struct SyncResult { ··· 339 369 340 370 let upstream = resolve_upstream(state, &fork.origin)?; 341 371 let refs = upstream_refs(state, &upstream, vec![branch.as_str().to_string()]).await?; 372 + if refs.object_format != fork.object_format { 373 + return Err(XrpcError::conflict(format!( 374 + "upstream stores {} objects but this fork stores {}", 375 + refs.object_format.capability(), 376 + fork.object_format.capability() 377 + ))); 378 + } 342 379 let tip = refs 343 380 .find(branch) 344 381 .ok_or_else(|| XrpcError::not_found("upstream repository doesn't have that branch"))?;
+32 -37
knot2/crates/knot-xrpc/src/repos.rs
··· 167 167 } 168 168 169 169 let input: CreateInput = decode(&body)?; 170 - let source = input 171 - .source 172 - .as_ref() 173 - .map(|source| { 174 - crate::forks::resolve_upstream(&state, source).map(|upstream| (source, upstream)) 175 - }) 176 - .transpose()?; 177 170 let head = input.default_branch.map(|branch| branch.head_ref()); 178 171 179 172 let owner = OwnerDid::new(actor.as_str()).expect("account DID is always a valid owner DID"); ··· 195 188 Resolved::Ready(None) => {} 196 189 } 197 190 191 + let fork = match &input.source { 192 + Some(origin) => Some(crate::forks::ForkSource::resolve(&state, origin).await?), 193 + None => None, 194 + }; 195 + let object_format = fork.as_ref().map(|fork| fork.refs.object_format); 196 + 198 197 let (repo_did, provisioning) = 199 198 provision_repo_did(&state, &actor, &input.rkey, input.repo_did).await?; 200 199 let now = state.now(); ··· 214 213 let placed = repo_did.clone(); 215 214 let lfs = lfs_store.clone(); 216 215 let provisioning = run_blocking(move || { 217 - let repo = layout.create(&placed).map_err(|error| match error { 216 + let repo = match object_format { 217 + Some(format) => layout.create_with_format(&placed, format), 218 + None => layout.create(&placed), 219 + } 220 + .map_err(|error| match error { 218 221 GitError::AlreadyExists(_) => XrpcError::conflict("repository already exists on disk"), 219 222 GitError::ReservedDid(_) => { 220 223 XrpcError::invalid_request("repoDid mustn't be knot's own identity") ··· 229 232 }) 230 233 .await?; 231 234 232 - let lfs_missing = match &source { 233 - Some((source_url, upstream)) => { 234 - match populate(&state, &repo_did, source_url, upstream).await { 235 - Ok(missing) => missing, 236 - Err(error) => { 237 - let layout = state.layout.clone(); 238 - let placed = repo_did.clone(); 239 - let lfs = lfs_store.clone(); 240 - let _ = run_blocking(move || { 241 - rollback_local(&layout, lfs.as_deref(), &placed); 242 - Ok(()) 243 - }) 244 - .await; 245 - return Err(error); 246 - } 235 + let lfs_missing = match &fork { 236 + Some(fork) => match populate(&state, &repo_did, fork).await { 237 + Ok(missing) => missing, 238 + Err(error) => { 239 + let layout = state.layout.clone(); 240 + let placed = repo_did.clone(); 241 + let lfs = lfs_store.clone(); 242 + let _ = run_blocking(move || { 243 + rollback_local(&layout, lfs.as_deref(), &placed); 244 + Ok(()) 245 + }) 246 + .await; 247 + return Err(error); 247 248 } 248 - } 249 + }, 249 250 None => Vec::new(), 250 251 }; 251 252 ··· 333 334 async fn populate<H: HttpTransport, C: Clock>( 334 335 state: &Arc<XrpcState<H, C>>, 335 336 repo_did: &RepoDid, 336 - source: &SourceUrl, 337 - upstream: &crate::forks::Upstream, 337 + fork: &crate::forks::ForkSource, 338 338 ) -> Result<Vec<knot_lfs::LfsOid>, XrpcError> { 339 - let prefixes = vec![ 340 - "HEAD".to_string(), 341 - "refs/heads/".to_string(), 342 - "refs/tags/".to_string(), 343 - ]; 344 - let refs = crate::forks::upstream_refs(state, upstream, prefixes).await?; 345 - let tips = refs.tips(); 339 + let tips = fork.refs.tips(); 346 340 let pack = crate::forks::upstream_pack( 347 341 state, 348 - upstream, 349 - knot_pack::WantOids::new(refs.tips()), 342 + &fork.upstream, 343 + knot_pack::WantOids::new(tips.clone()), 350 344 knot_pack::HaveOids::default(), 351 345 ) 352 346 .await?; 353 347 let layout = state.layout.clone(); 354 348 let placed = repo_did.clone(); 355 - let origin = source.clone(); 349 + let origin = fork.origin.clone(); 350 + let refs = fork.refs.clone(); 356 351 run_blocking(move || { 357 352 let repo = layout.open(&placed)?; 358 353 crate::forks::populate_fork(&repo, &refs, &pack, &origin) ··· 360 355 .await?; 361 356 crate::lfs::mirror_fork_objects( 362 357 Arc::clone(state), 363 - upstream.clone(), 358 + fork.upstream.clone(), 364 359 repo_did.clone(), 365 360 knot_pack::WantOids::new(tips), 366 361 knot_pack::HaveOids::default(),
+113 -23
knot2/crates/knot-xrpc/src/tests.rs
··· 267 267 events.remove(0) 268 268 } 269 269 270 - fn bootstrap(dir: &TempDir, rebuild: bool) -> (Layout, Arc<Index>, PathBuf) { 270 + fn bootstrap( 271 + dir: &TempDir, 272 + rebuild: bool, 273 + object_format: knot_types::ObjectFormat, 274 + ) -> (Layout, Arc<Index>, PathBuf) { 271 275 let scan_path = dir.path().join("repos"); 272 276 std::fs::create_dir_all(&scan_path).unwrap(); 273 277 let knot = knot_did(); 274 - let layout = Layout::new(&scan_path).reserving_meta(&knot).unwrap(); 278 + let layout = Layout::new(&scan_path) 279 + .with_object_format(object_format) 280 + .reserving_meta(&knot) 281 + .unwrap(); 275 282 layout.bootstrap_meta(&knot).unwrap(); 276 283 let meta_path = layout.meta_path(&knot).unwrap(); 277 284 let index = Arc::new(Index::new(meta_path.clone(), layout.clone())); ··· 425 432 426 433 impl World { 427 434 fn new() -> Self { 428 - Self::build(256, 256, AdmissionPolicy::Closed, None) 435 + Self::build( 436 + 256, 437 + 256, 438 + AdmissionPolicy::Closed, 439 + None, 440 + knot_types::ObjectFormat::SHA1, 441 + ) 429 442 } 430 443 431 444 fn open() -> Self { 432 - Self::build(256, 256, AdmissionPolicy::Open, None) 445 + Self::build( 446 + 256, 447 + 256, 448 + AdmissionPolicy::Open, 449 + None, 450 + knot_types::ObjectFormat::SHA1, 451 + ) 433 452 } 434 453 435 454 fn with_pending_limit(limit: usize) -> Self { 436 - Self::build(limit, limit, AdmissionPolicy::Closed, None) 455 + Self::build( 456 + limit, 457 + limit, 458 + AdmissionPolicy::Closed, 459 + None, 460 + knot_types::ObjectFormat::SHA1, 461 + ) 437 462 } 438 463 439 464 fn with_limits(global: usize, per_actor: usize) -> Self { 440 - Self::build(global, per_actor, AdmissionPolicy::Closed, None) 465 + Self::build( 466 + global, 467 + per_actor, 468 + AdmissionPolicy::Closed, 469 + None, 470 + knot_types::ObjectFormat::SHA1, 471 + ) 441 472 } 442 473 443 - fn with_git_http(git_http: Arc<dyn HttpTransport>) -> Self { 444 - Self::build(256, 256, AdmissionPolicy::Closed, Some(git_http)) 474 + fn with_git_http( 475 + git_http: Arc<dyn HttpTransport>, 476 + object_format: knot_types::ObjectFormat, 477 + ) -> Self { 478 + Self::build( 479 + 256, 480 + 256, 481 + AdmissionPolicy::Closed, 482 + Some(git_http), 483 + object_format, 484 + ) 445 485 } 446 486 447 487 fn build( ··· 449 489 per_actor: usize, 450 490 admission: AdmissionPolicy, 451 491 git_http: Option<Arc<dyn HttpTransport>>, 492 + object_format: knot_types::ObjectFormat, 452 493 ) -> Self { 453 494 let dir = tempfile::tempdir().unwrap(); 454 - let (layout, index, meta_path) = bootstrap(&dir, true); 495 + let (layout, index, meta_path) = bootstrap(&dir, true, object_format); 455 496 let admin = signer(1); 456 497 let member = signer(2); 457 498 let stranger = signer(3); ··· 514 555 515 556 fn build_state(responder: Responder, rebuild: bool) -> (TempDir, SharedState) { 516 557 let dir = tempfile::tempdir().unwrap(); 517 - let (layout, index, meta_path) = bootstrap(&dir, rebuild); 558 + let (layout, index, meta_path) = bootstrap(&dir, rebuild, knot_types::ObjectFormat::SHA1); 518 559 let state = state_from( 519 560 &dir, 520 561 (layout, index, meta_path), ··· 2634 2675 use knot_runtime::HttpTransport; 2635 2676 use knot_types::{Oid, RefName, UnixSeconds}; 2636 2677 2637 - const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; 2678 + const EMPTY_TREE_SHA1: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; 2679 + const EMPTY_TREE_SHA256: &str = 2680 + "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321"; 2681 + 2682 + fn empty_tree(repo: &Repo) -> Oid { 2683 + let hex = match repo.object_format() { 2684 + knot_types::ObjectFormat::SHA256 => EMPTY_TREE_SHA256, 2685 + _ => EMPTY_TREE_SHA1, 2686 + }; 2687 + Oid::from_hex(hex).unwrap() 2688 + } 2638 2689 2639 2690 fn ident(time: i64) -> Identity { 2640 2691 Identity { ··· 2648 2699 fn put_commit(repo: &Repo, parent: Option<Oid>, path: &str, content: &str, time: i64) -> Oid { 2649 2700 let base_tree = match parent { 2650 2701 Some(parent) => repo.find_commit(parent).unwrap().tree, 2651 - None => Oid::from_hex(EMPTY_TREE).unwrap(), 2702 + None => empty_tree(repo), 2652 2703 }; 2653 2704 let staged = vec![StagedChange { 2654 2705 path: knot_types::RepoPath::new(path).unwrap(), ··· 2991 3042 } 2992 3043 2993 3044 #[tokio::test] 2994 - async fn a_fork_from_a_remote_knot_fetches_over_http() { 3045 + async fn a_fork_over_http_takes_the_upstream_object_format_and_conflicts_when_it_changes() { 2995 3046 let upstream_dir = tempfile::tempdir().unwrap(); 2996 3047 let upstream_path = upstream_dir.path().join("uni.git"); 2997 - let upstream = Repo::create(&upstream_path).unwrap(); 2998 - upstream 2999 - .set_head(&RefName::new("refs/heads/main").unwrap()) 3000 - .unwrap(); 3048 + let upstream = 3049 + Repo::create_with_format(&upstream_path, knot_types::ObjectFormat::SHA1).unwrap(); 3050 + upstream.set_head(&main_ref()).unwrap(); 3001 3051 advance(&upstream, &main_ref(), "reef.txt", "kelp forest\n", 1_000); 3002 3052 let tip = advance(&upstream, &main_ref(), "tide.txt", "rock pool\n", 1_001); 3003 3053 3004 - let served = upstream_path.clone(); 3054 + let served = Arc::new(std::sync::RwLock::new(upstream_path.clone())); 3055 + let path = Arc::clone(&served); 3005 3056 let git_http: Arc<dyn HttpTransport> = 3006 3057 Arc::new(FakeHttp::new(move |request: &HttpRequest| { 3007 - let _keep = &upstream_dir; 3008 - let repo = Repo::open(&served).unwrap(); 3058 + let repo = Repo::open(path.read().unwrap().clone()).unwrap(); 3009 3059 let body = if request.url.path().ends_with("/info/refs") { 3010 3060 knot_pack::advertise_upload(&repo).unwrap() 3011 3061 } else { ··· 3019 3069 }) 3020 3070 })); 3021 3071 3022 - let world = World::with_git_http(git_http); 3072 + let world = World::with_git_http(git_http, knot_types::ObjectFormat::SHA256); 3023 3073 add_member_helper(&world).await; 3074 + let plain = create_repo_helper(&world, "kelp").await; 3075 + assert_eq!( 3076 + world.layout.open(&plain).unwrap().object_format(), 3077 + knot_types::ObjectFormat::SHA256, 3078 + "a repo with no upstream still uses this knot's configured format" 3079 + ); 3080 + 3024 3081 let remote = "https://barnacle.nel.pet/did:plc:squid/uni"; 3025 3082 let fork_did = fork_repo(&world, remote, "uni").await; 3026 3083 let fork = world.layout.open(&fork_did).unwrap(); 3084 + assert_eq!( 3085 + fork.object_format(), 3086 + knot_types::ObjectFormat::SHA1, 3087 + "a fork of a sha1 upstream must be sha1 so the upstream's objects ingest" 3088 + ); 3027 3089 assert_eq!(fork.find_ref(&main_ref()).unwrap(), Some(tip)); 3028 3090 assert_eq!(fork.origin_url().as_deref(), Some(remote)); 3091 + assert_eq!( 3092 + fork.read_blob( 3093 + fork.entry_at(tip, &knot_types::RepoPath::new("tide.txt").unwrap()) 3094 + .unwrap() 3095 + .unwrap() 3096 + .oid 3097 + ) 3098 + .unwrap(), 3099 + b"rock pool\n" 3100 + ); 3029 3101 3030 3102 let new_tip = advance( 3031 3103 &Repo::open(&upstream_path).unwrap(), ··· 3038 3110 sync_fork(&world, &world.member, MEMBER_HOST, "main").await, 3039 3111 StatusCode::OK 3040 3112 ); 3041 - let fork = world.layout.open(&fork_did).unwrap(); 3042 - assert_eq!(fork.find_ref(&main_ref()).unwrap(), Some(new_tip)); 3113 + assert_eq!( 3114 + world 3115 + .layout 3116 + .open(&fork_did) 3117 + .unwrap() 3118 + .find_ref(&main_ref()) 3119 + .unwrap(), 3120 + Some(new_tip) 3121 + ); 3122 + 3123 + let replaced = upstream_dir.path().join("replaced.git"); 3124 + let sha256 = Repo::create_with_format(&replaced, knot_types::ObjectFormat::SHA256).unwrap(); 3125 + sha256.set_head(&main_ref()).unwrap(); 3126 + advance(&sha256, &main_ref(), "reef.txt", "kelp forest\n", 1_000); 3127 + *served.write().unwrap() = replaced; 3128 + assert_eq!( 3129 + sync_fork(&world, &world.member, MEMBER_HOST, "main").await, 3130 + StatusCode::CONFLICT, 3131 + "the fork reports a format mismatch as a conflict" 3132 + ); 3043 3133 } 3044 3134 } 3045 3135