rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood4dabfaa701ea17a51d44dbd597b240c1b1961be2
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "895c2a12d379ae6f166314e063217ed1e8df393c",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"title": "node: Use `gix_packetline`",
"state": {
"status": "open",
"conflicts": []
},
"before": "c06b00e330d82c8b8221cc8f8776c883208d159f",
"after": "4dabfaa701ea17a51d44dbd597b240c1b1961be2",
"commits": [
"4dabfaa701ea17a51d44dbd597b240c1b1961be2"
],
"target": "c06b00e330d82c8b8221cc8f8776c883208d159f",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "895c2a12d379ae6f166314e063217ed1e8df393c",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "`gix-packetline` already is in the dependency closure of `radicle-node`.\nUse the well-tested and -used reader instead of ours.",
"base": "c06b00e330d82c8b8221cc8f8776c883208d159f",
"oid": "4dabfaa701ea17a51d44dbd597b240c1b1961be2",
"timestamp": 1771206639
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "d5e84c4b-a31d-42b5-b1be-667b685109f6"
},
"info_url": "https://cci.rad.levitte.org//d5e84c4b-a31d-42b5-b1be-667b685109f6.html"
}
Started at: 2026-02-16 02:51:25.500674+01:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/d5e84c4b-a31d-42b5-b1be-667b685109f6/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 138 issues · 35 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 895c2a12d379ae6f166314e063217ed1e8df393c
✓ Switched to branch patch/895c2a1 at revision 895c2a1
✓ Branch patch/895c2a1 setup to track rad/patches/895c2a12d379ae6f166314e063217ed1e8df393c
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 4dabfaa701ea17a51d44dbd597b240c1b1961be2
HEAD is now at 4dabfaa7 node: Use `gix_packetline`
Exit code: 0
$ rad patch show 895c2a12d379ae6f166314e063217ed1e8df393c -p
╭──────────────────────────────────────────────────────────────────────────╮
│ Title node: Use `gix_packetline` │
│ Patch 895c2a12d379ae6f166314e063217ed1e8df393c │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head 4dabfaa701ea17a51d44dbd597b240c1b1961be2 │
│ Base c06b00e330d82c8b8221cc8f8776c883208d159f │
│ Branches patch/895c2a1 │
│ Commits ahead 1, behind 0 │
│ Status open │
│ │
│ `gix-packetline` already is in the dependency closure of `radicle-node`. │
│ Use the well-tested and -used reader instead of ours. │
├──────────────────────────────────────────────────────────────────────────┤
│ 4dabfaa node: Use `gix_packetline` │
├──────────────────────────────────────────────────────────────────────────┤
│ ● Revision 895c2a1 @ 4dabfaa by lorenz z6MkkPv…WX5sTEz 48 seconds ago │
╰──────────────────────────────────────────────────────────────────────────╯
commit 4dabfaa701ea17a51d44dbd597b240c1b1961be2
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Mon Feb 16 02:20:55 2026 +0100
node: Use `gix_packetline`
`gix-packetline` already is in the dependency closure of `radicle-node`.
Use the well-tested and -used reader instead of ours.
diff --git a/Cargo.lock b/Cargo.lock
index 42d1d3785..d77d86ade 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3036,6 +3036,7 @@ dependencies = [
"crossbeam-channel",
"cyphernet",
"fastrand",
+ "gix-packetline",
"lexopt",
"log",
"mio 1.0.4",
diff --git a/Cargo.toml b/Cargo.toml
index 0946af707..f2809b5e3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ dunce = "1.0.5"
fastrand = { version = "2.0.0", default-features = false }
git2 = { version = "0.19.0", default-features = false, features = ["vendored-libgit2"] }
gix-hash = { version = "0.19.0", default-features = false }
+gix-packetline = { version = "0.19.1", default-features = false }
human-panic = "2.0.6"
itertools = "0.14"
lexopt = "0.3.0"
diff --git a/crates/radicle-node/Cargo.toml b/crates/radicle-node/Cargo.toml
index dd55ec2e6..d6afff16e 100644
--- a/crates/radicle-node/Cargo.toml
+++ b/crates/radicle-node/Cargo.toml
@@ -23,6 +23,7 @@ colored = { workspace = true }
crossbeam-channel = { workspace = true }
cyphernet = { workspace = true, features = ["tor", "dns", "ed25519", "p2p-ed25519", "noise-framework", "noise_sha2"] }
fastrand = { workspace = true }
+gix-packetline = { workspace = true, features = ["blocking-io"] }
lexopt = { workspace = true }
log = { workspace = true, features = ["kv", "std"] }
mio = { version = "1", features = ["net", "os-poll"] }
diff --git a/crates/radicle-node/src/worker.rs b/crates/radicle-node/src/worker.rs
index 0449b526f..49fc7e130 100644
--- a/crates/radicle-node/src/worker.rs
+++ b/crates/radicle-node/src/worker.rs
@@ -141,15 +141,52 @@ impl Worker {
let timeout = channels.timeout();
let (mut stream_r, stream_w) = channels.split();
- let header = match upload_pack::pktline::git_request(&mut stream_r) {
- Ok(header) => header,
- Err(e) => {
+
+ let mut iter = gix_packetline::StreamingPeekableIter::new(&mut stream_r,
+ &[gix_packetline::PacketLineRef::Flush],
+ false, /* packet tracing */
+ );
+
+ let header = match iter.read_line() {
+ None => {
+ return FetchResult::Responder {
+ rid: None,
+ result: Err(UploadError::PacketLine(std::io::Error::new(
+ std::io::ErrorKind::UnexpectedEof,
+ "unexpected end of stream while reading upload-pack header",
+ ))),
+ }
+ }
+ Some(Err(e)) => {
return FetchResult::Responder {
rid: None,
result: Err(UploadError::PacketLine(e)),
}
}
+ Some(Ok(Err(e))) => {
+ return FetchResult::Responder {
+ rid: None,
+ result: Err(
+ UploadError::PacketLine(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ format!("invalid upload-pack header: {e}"),
+ )),
+ ),
+ }
+ }
+ Some(Ok(Ok(header))) => header,
};
+
+ let Ok(header) = upload_pack::GitRequest::try_from(header) else {
+ return FetchResult::Responder {
+ rid: None,
+ result: Err(UploadError::PacketLine(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ "failed to parse upload-pack header",
+ ))),
+ };
+ };
+
log::debug!(target: "worker", "Spawning upload-pack process for {} on stream {stream}..", header.repo);
if let Err(e) = self.is_authorized(remote, header.repo) {
diff --git a/crates/radicle-node/src/worker/upload_pack.rs b/crates/radicle-node/src/worker/upload_pack.rs
index d36b30ff7..bca9d02ef 100644
--- a/crates/radicle-node/src/worker/upload_pack.rs
+++ b/crates/radicle-node/src/worker/upload_pack.rs
@@ -25,7 +25,7 @@ pub fn upload_pack<R, W>(
remote: NodeId,
storage: &Storage,
emitter: &Emitter<Event>,
- header: &pktline::GitRequest,
+ header: &GitRequest,
mut recv: R,
send: W,
timeout: Duration,
@@ -239,119 +239,65 @@ where
}
}
-pub(super) mod pktline {
- use std::io;
- use std::io::Read;
- use std::str;
-
- use radicle::prelude::RepoId;
-
- pub const HEADER_LEN: usize = 4;
-
- /// Read and parse the `GitRequest` data from the client side.
- pub fn git_request<R>(reader: &mut R) -> io::Result<GitRequest>
- where
- R: io::Read,
- {
- let mut reader = Reader::new(reader);
- let (header, _) = reader.read_request_pktline()?;
- Ok(header)
- }
-
- struct Reader<'a, R> {
- stream: &'a mut R,
- }
-
- impl<'a, R: io::Read> Reader<'a, R> {
- /// Create a new packet-line reader.
- pub fn new(stream: &'a mut R) -> Self {
- Self { stream }
- }
-
- /// Parse a Git request packet-line.
- ///
- /// Example: `0032git-upload-pack /project.git\0host=myserver.com\0`
- ///
- fn read_request_pktline(&mut self) -> io::Result<(GitRequest, Vec<u8>)> {
- let mut pktline = [0u8; 1024];
- let length = self.read_pktline(&mut pktline)?;
- let Some(cmd) = GitRequest::parse(&pktline[4..length]) else {
- return Err(io::ErrorKind::InvalidInput.into());
- };
- Ok((cmd, Vec::from(&pktline[..length])))
- }
-
- /// Parse a Git packet-line.
- fn read_pktline(&mut self, buf: &mut [u8]) -> io::Result<usize> {
- self.read_exact(&mut buf[..HEADER_LEN])?;
-
- let length = str::from_utf8(&buf[..HEADER_LEN])
- .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?;
- let length = usize::from_str_radix(length, 16)
- .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?;
-
- self.read_exact(&mut buf[HEADER_LEN..length])?;
-
- Ok(length)
- }
- }
+/// The Git request packet-line for a repository.
+///
+/// See <https://git-scm.com/docs/pack-protocol.html#_git_transport>.
+///
+/// Example: `0032git-upload-pack /rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5.git\0host=myserver.com\0`
+#[derive(Debug)]
+pub struct GitRequest {
+ pub repo: RepoId,
+ #[allow(dead_code)]
+ pub path: String,
+ #[allow(dead_code)]
+ pub host: Option<(String, Option<u16>)>,
+ pub extra: Vec<(String, Option<String>)>,
+}
- impl<R: io::Read> io::Read for Reader<'_, R> {
- fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
- self.stream.read(buf)
- }
- }
+impl GitRequest {
+ /// Parse a Git command from a packet-line.
+ fn parse(input: &[u8]) -> Option<Self> {
+ let input = str::from_utf8(input).ok()?;
+ let mut parts = input
+ .strip_prefix("git-upload-pack ")?
+ .split_terminator('\0');
+
+ let path = parts.next()?.to_owned();
+ let repo = path.strip_prefix('/')?.parse().ok()?;
+ let host = match parts.next() {
+ None | Some("") => None,
+ Some(host) => {
+ let host = host.strip_prefix("host=")?;
+ match host.split_once(':') {
+ None => Some((host.to_owned(), None)),
+ Some((host, port)) => {
+ let port = port.parse::<u16>().ok()?;
+ Some((host.to_owned(), Some(port)))
+ }
+ }
+ }
+ };
+ let extra = parts
+ .skip_while(|part| part.is_empty())
+ .map(|part| match part.split_once('=') {
+ None => (part.to_owned(), None),
+ Some((k, v)) => (k.to_owned(), Some(v.to_owned())),
+ })
+ .collect();
- /// The Git request packet-line for a Heartwood repository.
- ///
- /// Example: `0032git-upload-pack /rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5.git\0host=myserver.com\0`
- #[derive(Debug)]
- pub struct GitRequest {
- pub repo: RepoId,
- #[allow(dead_code)]
- pub path: String,
- #[allow(dead_code)]
- pub host: Option<(String, Option<u16>)>,
- pub extra: Vec<(String, Option<String>)>,
+ Some(Self {
+ repo,
+ path,
+ host,
+ extra,
+ })
}
+}
- impl GitRequest {
- /// Parse a Git command from a packet-line.
- fn parse(input: &[u8]) -> Option<Self> {
- let input = str::from_utf8(input).ok()?;
- let mut parts = input
- .strip_prefix("git-upload-pack ")?
- .split_terminator('\0');
-
- let path = parts.next()?.to_owned();
- let repo = path.strip_prefix('/')?.parse().ok()?;
- let host = match parts.next() {
- None | Some("") => None,
- Some(host) => {
- let host = host.strip_prefix("host=")?;
- match host.split_once(':') {
- None => Some((host.to_owned(), None)),
- Some((host, port)) => {
- let port = port.parse::<u16>().ok()?;
- Some((host.to_owned(), Some(port)))
- }
- }
- }
- };
- let extra = parts
- .skip_while(|part| part.is_empty())
- .map(|part| match part.split_once('=') {
- None => (part.to_owned(), None),
- Some((k, v)) => (k.to_owned(), Some(v.to_owned())),
- })
- .collect();
+impl TryFrom<gix_packetline::PacketLineRef<'_>> for GitRequest {
+ type Error = ();
- Some(Self {
- repo,
- path,
- host,
- extra,
- })
- }
+ fn try_from(value: gix_packetline::PacketLineRef<'_>) -> Result<Self, Self::Error> {
+ value.as_slice().and_then(Self::parse).ok_or(())
}
}
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps --all-features cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name d5e84c4b-a31d-42b5-b1be-667b685109f6 -v /opt/radcis/ci.rad.levitte.org/cci/state/d5e84c4b-a31d-42b5-b1be-667b685109f6/s:/d5e84c4b-a31d-42b5-b1be-667b685109f6/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/d5e84c4b-a31d-42b5-b1be-667b685109f6/w:/d5e84c4b-a31d-42b5-b1be-667b685109f6/w -w /d5e84c4b-a31d-42b5-b1be-667b685109f6/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /d5e84c4b-a31d-42b5-b1be-667b685109f6/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.90-x86_64-unknown-linux-gnu'
info: latest update on 2025-09-18, rust version 1.90.0 (1159e78c4 2025-09-14)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.90.0 (840b83a10 2025-07-30)
+ rustc --version
rustc 1.90.0 (1159e78c4 2025-09-14)
+ cargo fmt --check
Diff in /d5e84c4b-a31d-42b5-b1be-667b685109f6/w/crates/radicle-node/src/worker/upload_pack.rs:240:
}
/// The Git request packet-line for a repository.
-///
+///
/// See <https://git-scm.com/docs/pack-protocol.html#_git_transport>.
///
/// Example: `0032git-upload-pack /rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5.git\0host=myserver.com\0`
Diff in /d5e84c4b-a31d-42b5-b1be-667b685109f6/w/crates/radicle-node/src/worker.rs:142:
let timeout = channels.timeout();
let (mut stream_r, stream_w) = channels.split();
- let mut iter = gix_packetline::StreamingPeekableIter::new(&mut stream_r,
+ let mut iter = gix_packetline::StreamingPeekableIter::new(
+ &mut stream_r,
&[gix_packetline::PacketLineRef::Flush],
false, /* packet tracing */
);
Diff in /d5e84c4b-a31d-42b5-b1be-667b685109f6/w/crates/radicle-node/src/worker.rs:166:
Some(Ok(Err(e))) => {
return FetchResult::Responder {
rid: None,
- result: Err(
- UploadError::PacketLine(std::io::Error::new(
- std::io::ErrorKind::InvalidData,
- format!("invalid upload-pack header: {e}"),
- )),
- ),
+ result: Err(UploadError::PacketLine(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ format!("invalid upload-pack header: {e}"),
+ ))),
}
}
Some(Ok(Ok(header))) => header,
Exit code: 1
{
"response": "finished",
"result": "failure"
}