This repository has no description
0

Configure Feed

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

core / knot2 / crates / knot-pack / tests / fetch.rs
10 kB 334 lines
1use std::io::Write; 2use std::path::{Path, PathBuf}; 3use std::process::Stdio; 4use std::sync::Arc; 5 6use axum::http; 7use knot_git::{Layout, Repo, Staging}; 8use knot_pack::{FetchError, HaveOids, PackLimits, WantOids, ingest_pack, local_pack, local_refs}; 9use knot_runtime::{FakeHttp, HttpRequest, HttpResponse, HttpTransport}; 10use knot_types::{Oid, RefName, RepoDid}; 11use url::Url; 12 13mod common; 14use common::{commit, must, pack_objects}; 15 16fn seed_source(dir: &Path) -> PathBuf { 17 let work = dir.join("work"); 18 std::fs::create_dir_all(&work).unwrap(); 19 must(&work, &["init", "-q", "-b", "main"]); 20 commit(&work, "reef.txt", "kelp forest\n", "first"); 21 commit(&work, "tide.txt", "rock pool\n", "second"); 22 must(&work, &["tag", "v1"]); 23 must(&work, &["branch", "anemone"]); 24 let bare = dir.join("source.git"); 25 must( 26 dir, 27 &[ 28 "clone", 29 "-q", 30 "--bare", 31 work.to_str().unwrap(), 32 bare.to_str().unwrap(), 33 ], 34 ); 35 bare 36} 37 38fn stock_git_server(repo: PathBuf) -> Arc<dyn HttpTransport> { 39 Arc::new(FakeHttp::new(move |request: &HttpRequest| { 40 let response = |body: Vec<u8>| { 41 Ok(HttpResponse { 42 status: http::StatusCode::OK, 43 headers: http::HeaderMap::new(), 44 body: body.into(), 45 }) 46 }; 47 if request.url.path().ends_with("/info/refs") { 48 let out = knot_fixtures::command(&repo) 49 .args([ 50 "upload-pack", 51 "--stateless-rpc", 52 "--http-backend-info-refs", 53 ".", 54 ]) 55 .env("GIT_PROTOCOL", "version=2") 56 .output() 57 .expect("git upload-pack advertises"); 58 assert!(out.status.success()); 59 let mut body = b"001e# service=git-upload-pack\n0000".to_vec(); 60 body.extend_from_slice(&out.stdout); 61 return response(body); 62 } 63 let mut child = knot_fixtures::command(&repo) 64 .args(["upload-pack", "--stateless-rpc", "."]) 65 .env("GIT_PROTOCOL", "version=2") 66 .stdin(Stdio::piped()) 67 .stdout(Stdio::piped()) 68 .stderr(Stdio::piped()) 69 .spawn() 70 .expect("git upload-pack serves"); 71 child 72 .stdin 73 .take() 74 .unwrap() 75 .write_all(request.body.as_deref().unwrap_or_default()) 76 .unwrap(); 77 let out = child.wait_with_output().expect("git upload-pack finishes"); 78 assert!( 79 out.status.success(), 80 "upload-pack: {}", 81 String::from_utf8_lossy(&out.stderr) 82 ); 83 response(out.stdout) 84 })) 85} 86 87fn knot_server(repo_path: PathBuf) -> Arc<dyn HttpTransport> { 88 Arc::new(FakeHttp::new(move |request: &HttpRequest| { 89 let repo = Repo::open(&repo_path).unwrap(); 90 let body = if request.url.path().ends_with("/info/refs") { 91 knot_pack::advertise_upload(&repo).unwrap() 92 } else { 93 knot_pack::upload_pack(&repo, request.body.as_deref().unwrap_or_default()).unwrap() 94 }; 95 Ok(HttpResponse { 96 status: http::StatusCode::OK, 97 headers: http::HeaderMap::new(), 98 body: body.into(), 99 }) 100 })) 101} 102 103fn base_url() -> Url { 104 Url::parse("https://kelp.oyster.cafe/did:plc:squid/uni").unwrap() 105} 106 107const CAP: u64 = 64 * 1024 * 1024; 108 109fn refnames(records: &[knot_git::RefRecord]) -> Vec<&str> { 110 records.iter().map(|record| record.name.as_str()).collect() 111} 112 113async fn clone_through(http: &dyn HttpTransport, source: &Repo, target: &Repo) { 114 let refs = knot_pack::remote_refs(http, &base_url(), &["HEAD", "refs/heads/", "refs/tags/"]) 115 .await 116 .unwrap(); 117 assert_eq!( 118 refs.head_symref.as_ref().map(RefName::as_str), 119 Some("refs/heads/main") 120 ); 121 let mut expected = source.advertised_refs().unwrap().to_vec(); 122 expected.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str())); 123 let mut got = refs.refs.clone(); 124 got.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str())); 125 assert_eq!(refnames(&got), refnames(&expected)); 126 assert_eq!(got, expected); 127 128 let pack = knot_pack::remote_pack( 129 http, 130 &base_url(), 131 &WantOids::new(refs.tips()), 132 &HaveOids::default(), 133 CAP, 134 ) 135 .await 136 .unwrap(); 137 ingest_pack( 138 &target.objects_dir(), 139 &pack, 140 &PackLimits::default(), 141 target.object_format().kind(), 142 ) 143 .unwrap(); 144 let closure = target 145 .select_pack_objects( 146 knot_git::Wants::new(&refs.tips()), 147 knot_git::Haves::new(&[]), 148 ) 149 .unwrap(); 150 assert!(closure.iter().all(|oid| target.contains(*oid))); 151} 152 153#[tokio::test] 154async fn the_client_clones_from_both_stock_git_and_native_knot_servers() { 155 let dir = tempfile::tempdir().unwrap(); 156 let source_path = seed_source(dir.path()); 157 let source = Repo::open(&source_path).unwrap(); 158 159 let stock = Repo::create(dir.path().join("fork-stock.git")).unwrap(); 160 clone_through( 161 stock_git_server(source_path.clone()).as_ref(), 162 &source, 163 &stock, 164 ) 165 .await; 166 167 let knot = Repo::create(dir.path().join("fork-knot.git")).unwrap(); 168 clone_through(knot_server(source_path.clone()).as_ref(), &source, &knot).await; 169} 170 171#[tokio::test] 172async fn an_incremental_pull_completes_through_staging() { 173 let dir = tempfile::tempdir().unwrap(); 174 let source_path = seed_source(dir.path()); 175 let source = Repo::open(&source_path).unwrap(); 176 let clone = Repo::create(dir.path().join("fork.git")).unwrap(); 177 let http = knot_server(source_path.clone()); 178 clone_through(http.as_ref(), &source, &clone).await; 179 let main = RefName::new("refs/heads/main").unwrap(); 180 let old_tip = source.find_ref(&main).unwrap().unwrap(); 181 clone 182 .update_ref(&knot_git::RefUpdate::Create { 183 name: main.clone(), 184 new: old_tip, 185 }) 186 .unwrap(); 187 188 let work = dir.path().join("work"); 189 commit(&work, "spray.txt", "salt\n", "third"); 190 let new_hex = must(&work, &["rev-parse", "HEAD"]); 191 let new_tip = Oid::from_hex(&new_hex).unwrap(); 192 let oids: Vec<String> = must( 193 &work, 194 &[ 195 "rev-list", 196 "--objects", 197 &new_hex, 198 "--not", 199 &old_tip.to_hex(), 200 ], 201 ) 202 .lines() 203 .filter_map(|line| line.split_whitespace().next()) 204 .map(str::to_string) 205 .collect(); 206 let pack = pack_objects(&work, &oids); 207 ingest_pack( 208 &source.objects_dir(), 209 &pack, 210 &PackLimits::default(), 211 source.object_format().kind(), 212 ) 213 .unwrap(); 214 source 215 .update_ref(&knot_git::RefUpdate::Update { 216 name: main.clone(), 217 old: old_tip, 218 new: new_tip, 219 }) 220 .unwrap(); 221 assert_ne!(old_tip, new_tip); 222 223 let pack = knot_pack::remote_pack( 224 http.as_ref(), 225 &base_url(), 226 &WantOids::new(vec![new_tip]), 227 &HaveOids::new(vec![old_tip]), 228 CAP, 229 ) 230 .await 231 .unwrap(); 232 let staging = Staging::new(&clone).unwrap(); 233 ingest_pack( 234 &staging.repo().objects_dir(), 235 &pack, 236 &PackLimits::default(), 237 clone.object_format().kind(), 238 ) 239 .unwrap(); 240 let closure = staging 241 .repo() 242 .select_pack_objects( 243 knot_git::Wants::new(&[new_tip]), 244 knot_git::Haves::new(&[old_tip]), 245 ) 246 .unwrap(); 247 assert!(closure.iter().all(|oid| staging.repo().contains(*oid))); 248 staging.migrate_into(&clone).unwrap(); 249 assert!(clone.contains(new_tip)); 250} 251 252#[tokio::test] 253async fn a_tiny_pack_limit_refuses_both_remote_and_local_transfers() { 254 let dir = tempfile::tempdir().unwrap(); 255 let source_path = seed_source(dir.path()); 256 let source = Repo::open(&source_path).unwrap(); 257 let tips: Vec<Oid> = source 258 .advertised_refs() 259 .unwrap() 260 .iter() 261 .map(|record| record.target) 262 .collect(); 263 264 let http = knot_server(source_path); 265 let remote = knot_pack::remote_pack( 266 http.as_ref(), 267 &base_url(), 268 &WantOids::new(tips.clone()), 269 &HaveOids::default(), 270 16, 271 ) 272 .await; 273 assert!(matches!( 274 remote, 275 Err(FetchError::PackTooLarge { limit: 16 }) 276 )); 277 278 let local = local_pack(&source, &WantOids::new(tips), &HaveOids::default(), 16); 279 assert!(matches!(local, Err(FetchError::PackTooLarge { limit: 16 }))); 280} 281 282#[test] 283fn local_refs_and_pack_mirror_a_same_knot_source() { 284 let dir = tempfile::tempdir().unwrap(); 285 let source_path = seed_source(dir.path()); 286 let source = Repo::open(&source_path).unwrap(); 287 let hidden = RefName::new("refs/hidden/feature/main").unwrap(); 288 let main = RefName::new("refs/heads/main").unwrap(); 289 let tip = source.find_ref(&main).unwrap().unwrap(); 290 source 291 .update_ref(&knot_git::RefUpdate::Create { 292 name: hidden, 293 new: tip, 294 }) 295 .unwrap(); 296 297 let refs = local_refs(&source, &["HEAD", "refs/heads/", "refs/tags/"]).unwrap(); 298 assert_eq!( 299 refs.head_symref.as_ref().map(RefName::as_str), 300 Some("refs/heads/main") 301 ); 302 assert!( 303 refs.refs 304 .iter() 305 .all(|record| !record.name.as_str().starts_with("refs/hidden/")), 306 "hidden ref must never leave source repo through a fork" 307 ); 308 309 let layout = Layout::new(dir.path().join("scan")); 310 let fork = layout 311 .create(&RepoDid::new("did:plc:limpet").unwrap()) 312 .unwrap(); 313 let pack = local_pack( 314 &source, 315 &WantOids::new(refs.tips()), 316 &HaveOids::default(), 317 CAP, 318 ) 319 .unwrap(); 320 ingest_pack( 321 &fork.objects_dir(), 322 &pack, 323 &PackLimits::default(), 324 fork.object_format().kind(), 325 ) 326 .unwrap(); 327 let closure = fork 328 .select_pack_objects( 329 knot_git::Wants::new(&refs.tips()), 330 knot_git::Haves::new(&[]), 331 ) 332 .unwrap(); 333 assert!(closure.iter().all(|oid| fork.contains(*oid))); 334}