Skip to content

Commit 32bc5c7

Browse files
feat: wire up serve
1 parent 97e7f7f commit 32bc5c7

5 files changed

Lines changed: 226 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-serve/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,25 @@ doctest = false
1717
[features]
1818
## Enable support for SHA-1 hashes.
1919
sha1 = ["gix-hash/sha1"]
20+
## Blocking server support.
21+
blocking-server = ["gix-protocol/blocking-server", "gix-transport/blocking-server"]
2022

2123
[dependencies]
2224
gix-ref = { version = "^0.60.0", path = "../gix-ref" }
2325
gix-hash = { version = "^0.22.1", path = "../gix-hash" }
2426
gix-pack = { version = "^0.67.0", path = "../gix-pack", features = ["generate"] }
2527
gix-object = { version = "^0.57.0", path = "../gix-object" }
2628
gix-features = { version = "^0.46.1", path = "../gix-features" }
29+
gix-protocol = { version = "^0.58.0", path = "../gix-protocol", optional = true }
30+
gix-transport = { version = "^0.55.0", path = "../gix-transport", optional = true }
2731
bstr = { version = "1.12.0", default-features = false, features = ["std"] }
2832
thiserror = "2.0.18"
2933

3034
[dev-dependencies]
3135
gix-testtools = { path = "../tests/tools" }
3236
gix-odb = { version = "^0.77.0", path = "../gix-odb" }
37+
gix-packetline = { version = "^0.21.1", path = "../gix-packetline", features = ["blocking-io"] }
38+
gix-transport = { version = "^0.55.0", path = "../gix-transport", features = ["blocking-server"] }
3339

3440
[package.metadata.docs.rs]
3541
all-features = true

gix-serve/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
pub mod pack;
66
///
77
pub mod refs;
8+
///
9+
#[cfg(feature = "blocking-server")]
10+
pub mod serve;
811

912
use bstr::BString;
1013
use gix_hash::ObjectId;

gix-serve/src/serve.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use std::io::{self, Read, Write};
2+
3+
use gix_hash::{oid, ObjectId};
4+
use gix_pack::Find;
5+
use gix_protocol::serve::{
6+
upload_pack::{self, serve_upload_pack_v1, serve_upload_pack_v2},
7+
RefAdvertisement,
8+
};
9+
use gix_ref::file::Store;
10+
use gix_transport::{server::blocking_io::connection::Connection, Protocol};
11+
12+
use crate::{refs::collect_refs, AdvertisableRef};
13+
14+
impl AdvertisableRef {
15+
/// Borrow as a `RefAdvertisement` for the protocol layer.
16+
pub fn as_advertisement(&self) -> RefAdvertisement<'_> {
17+
RefAdvertisement {
18+
name: &self.name,
19+
object_id: &self.object_id,
20+
peeled: self.peeled.as_deref(),
21+
symref_target: self.symref_target.as_ref().map(|s| s.as_ref()),
22+
}
23+
}
24+
}
25+
26+
/// Errors from serving upload-pack.
27+
#[derive(Debug, thiserror::Error)]
28+
#[allow(missing_docs)]
29+
pub enum Error {
30+
#[error(transparent)]
31+
Refs(#[from] crate::Error),
32+
#[error(transparent)]
33+
Protocol(#[from] upload_pack::Error),
34+
#[error(transparent)]
35+
Pack(#[from] crate::pack::Error),
36+
}
37+
38+
/// Serve an upload-pack session over a connection.
39+
pub fn serve_upload_pack<R: Read, W: Write, F: Find + Send + Clone + 'static>(
40+
ref_store: &Store,
41+
db: F,
42+
connection: &mut Connection<R, W>,
43+
protocol: Protocol,
44+
) -> Result<(), Error> {
45+
let owned_refs = collect_refs(ref_store)?;
46+
let refs: Vec<_> = owned_refs.iter().map(AdvertisableRef::as_advertisement).collect();
47+
48+
let has_object = |oid: &oid| db.contains(oid);
49+
50+
let generate_pack = |wants: &[ObjectId], haves: &[ObjectId], out: &mut dyn Write| {
51+
crate::pack::generate_pack(db.clone(), wants, haves, out).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
52+
};
53+
54+
match protocol {
55+
Protocol::V1 | Protocol::V0 => {
56+
serve_upload_pack_v1(connection, &refs, has_object, generate_pack, &[])?;
57+
}
58+
Protocol::V2 => {
59+
serve_upload_pack_v2(
60+
connection,
61+
&refs,
62+
has_object,
63+
generate_pack,
64+
&[("fetch", None), ("ls-refs", None)],
65+
)?;
66+
}
67+
}
68+
69+
Ok(())
70+
}

gix-serve/tests/upload_pack.rs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
use std::sync::Arc;
2+
3+
use gix_hash::ObjectId;
4+
use gix_odb::store::init::Options as OdbOptions;
5+
use gix_packetline::blocking_io::encode::{data_to_write, delim_to_write, flush_to_write};
6+
use gix_ref::file::Store;
7+
use gix_ref::store::init::Options as RefOptions;
8+
use gix_serve::refs::collect_refs;
9+
use gix_serve::serve::serve_upload_pack;
10+
use gix_transport::server::blocking_io::connection::Connection;
11+
use gix_transport::{Protocol, Service};
12+
13+
fn ref_store(script: &str) -> Store {
14+
let path = gix_testtools::scripted_fixture_read_only_standalone(script).expect("fixture");
15+
Store::at(
16+
path.join(".git"),
17+
RefOptions {
18+
write_reflog: gix_ref::store::WriteReflog::Disable,
19+
object_hash: gix_hash::Kind::Sha1,
20+
precompose_unicode: false,
21+
prohibit_windows_device_names: false,
22+
},
23+
)
24+
}
25+
26+
fn odb(script: &str) -> gix_odb::HandleArc {
27+
let path = gix_testtools::scripted_fixture_read_only_standalone(script).expect("fixture");
28+
let store =
29+
gix_odb::Store::at_opts(path.join(".git/objects"), &mut None.into_iter(), OdbOptions::default()).expect("odb");
30+
let mut cache = Arc::new(store).to_cache_arc();
31+
cache.prevent_pack_unload();
32+
cache
33+
}
34+
35+
fn build_v1_want_done(oid: &ObjectId) -> Vec<u8> {
36+
let mut buf = Vec::new();
37+
data_to_write(format!("want {}\n", oid.to_hex()).as_bytes(), &mut buf).unwrap();
38+
flush_to_write(&mut buf).unwrap();
39+
data_to_write(b"done\n", &mut buf).unwrap();
40+
flush_to_write(&mut buf).unwrap();
41+
buf
42+
}
43+
44+
fn build_v2_fetch_input(wants: &[ObjectId]) -> Vec<u8> {
45+
let mut buf = Vec::new();
46+
data_to_write(b"command=fetch\n", &mut buf).unwrap();
47+
delim_to_write(&mut buf).unwrap();
48+
for oid in wants {
49+
data_to_write(format!("want {}\n", oid.to_hex()).as_bytes(), &mut buf).unwrap();
50+
}
51+
data_to_write(b"done\n", &mut buf).unwrap();
52+
flush_to_write(&mut buf).unwrap();
53+
buf
54+
}
55+
56+
fn assert_has_valid_pack(output: &[u8]) {
57+
let pack_pos = output.windows(4).position(|w| w == b"PACK");
58+
assert!(pack_pos.is_some(), "output should contain pack data");
59+
60+
let pack_start = pack_pos.unwrap();
61+
assert_eq!(
62+
&output[pack_start + 4..pack_start + 8],
63+
&[0, 0, 0, 2],
64+
"pack format version 2"
65+
);
66+
67+
let num_entries = u32::from_be_bytes(output[pack_start + 8..pack_start + 12].try_into().unwrap());
68+
assert!(num_entries > 0, "pack should contain objects");
69+
}
70+
71+
#[test]
72+
fn v1_fresh_clone() {
73+
let store = ref_store("make_repo_simple.sh");
74+
let db = odb("make_repo_simple.sh");
75+
let refs = collect_refs(&store).unwrap();
76+
let main_oid = refs.iter().find(|r| r.name == "refs/heads/main").unwrap().object_id;
77+
78+
let input = build_v1_want_done(&main_oid);
79+
let mut output = Vec::new();
80+
let mut conn = Connection::new(
81+
&input[..],
82+
&mut output,
83+
Service::UploadPack,
84+
"/repo.git",
85+
Protocol::V1,
86+
false,
87+
);
88+
89+
serve_upload_pack(&store, db, &mut conn, Protocol::V1).unwrap();
90+
91+
assert!(!output.is_empty());
92+
let nak_pos = output.windows(3).position(|w| w == b"NAK");
93+
let pack_pos = output.windows(4).position(|w| w == b"PACK").unwrap();
94+
assert!(nak_pos.is_some());
95+
assert!(nak_pos.unwrap() < pack_pos);
96+
assert_has_valid_pack(&output);
97+
}
98+
99+
#[test]
100+
fn v1_empty_wants() {
101+
let store = ref_store("make_repo_simple.sh");
102+
let db = odb("make_repo_simple.sh");
103+
104+
let mut input = Vec::new();
105+
flush_to_write(&mut input).unwrap();
106+
let mut output = Vec::new();
107+
let mut conn = Connection::new(
108+
&input[..],
109+
&mut output,
110+
Service::UploadPack,
111+
"/repo.git",
112+
Protocol::V1,
113+
false,
114+
);
115+
116+
serve_upload_pack(&store, db, &mut conn, Protocol::V1).unwrap();
117+
118+
assert!(!output.is_empty());
119+
assert!(output.windows(4).position(|w| w == b"PACK").is_none());
120+
}
121+
122+
#[test]
123+
fn v2_fresh_clone() {
124+
let store = ref_store("make_repo_simple.sh");
125+
let db = odb("make_repo_simple.sh");
126+
let refs = collect_refs(&store).unwrap();
127+
let main_oid = refs.iter().find(|r| r.name == "refs/heads/main").unwrap().object_id;
128+
129+
let input = build_v2_fetch_input(&[main_oid]);
130+
let mut output = Vec::new();
131+
let mut conn = Connection::new(
132+
&input[..],
133+
&mut output,
134+
Service::UploadPack,
135+
"/repo.git",
136+
Protocol::V2,
137+
false,
138+
);
139+
140+
serve_upload_pack(&store, db, &mut conn, Protocol::V2).unwrap();
141+
142+
assert!(!output.is_empty());
143+
assert_has_valid_pack(&output);
144+
}

0 commit comments

Comments
 (0)