1use std::{fmt, pin::Pin, str::FromStr};
5
6use bytes::Bytes;
7use futures::stream::Stream;
8
9pub type ProtocolStream = Pin<Box<dyn Stream<Item = Result<Bytes, ProtocolError>> + Send>>;
11
12#[derive(Debug, thiserror::Error)]
14pub enum ProtocolError {
15 #[error("Invalid service: {0}")]
16 InvalidService(String),
17
18 #[error("Repository not found: {0}")]
19 RepositoryNotFound(String),
20
21 #[error("Object not found: {0}")]
22 ObjectNotFound(String),
23
24 #[error("Invalid request: {0}")]
25 InvalidRequest(String),
26
27 #[error("Unauthorized: {0}")]
28 Unauthorized(String),
29
30 #[error("IO error: {0}")]
31 Io(#[from] std::io::Error),
32
33 #[error("Pack error: {0}")]
34 Pack(String),
35
36 #[error("Internal error: {0}")]
37 Internal(String),
38}
39
40impl ProtocolError {
41 pub fn invalid_service(service: &str) -> Self {
42 ProtocolError::InvalidService(service.to_string())
43 }
44
45 pub fn repository_error(msg: String) -> Self {
46 ProtocolError::Internal(msg)
47 }
48
49 pub fn invalid_request(msg: &str) -> Self {
50 ProtocolError::InvalidRequest(msg.to_string())
51 }
52
53 pub fn unauthorized(msg: &str) -> Self {
54 ProtocolError::Unauthorized(msg.to_string())
55 }
56}
57
58#[derive(Debug, PartialEq, Clone, Copy, Default)]
60pub enum TransportProtocol {
61 Local,
62 #[default]
63 Http,
64 Ssh,
65 Git,
66}
67
68#[derive(Debug, PartialEq, Clone, Copy)]
70pub enum ServiceType {
71 UploadPack,
72 ReceivePack,
73}
74
75impl fmt::Display for ServiceType {
76 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
77 match self {
78 ServiceType::UploadPack => write!(f, "git-upload-pack"),
79 ServiceType::ReceivePack => write!(f, "git-receive-pack"),
80 }
81 }
82}
83
84impl FromStr for ServiceType {
85 type Err = ProtocolError;
86
87 fn from_str(s: &str) -> Result<Self, Self::Err> {
88 match s {
89 "git-upload-pack" => Ok(ServiceType::UploadPack),
90 "git-receive-pack" => Ok(ServiceType::ReceivePack),
91 _ => Err(ProtocolError::InvalidService(s.to_string())),
92 }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq)]
118pub enum Capability {
119 MultiAck,
121 MultiAckDetailed,
123 NoDone,
125 SideBand,
127 SideBand64k,
129 ReportStatus,
131 ReportStatusv2,
133 OfsDelta,
135 DeepenSince,
137 DeepenNot,
139 DeepenRelative,
141 ThinPack,
143 Shallow,
145 IncludeTag,
147 DeleteRefs,
149 Quiet,
151 Atomic,
153 NoThin,
155 NoProgress,
157 AllowTipSha1InWant,
159 AllowReachableSha1InWant,
161 PushCert(String),
163 PushOptions,
165 ObjectFormat(String),
167 SessionId(String),
169 Filter(String),
171 Symref(String),
173 Agent(String),
175 Unknown(String),
177}
178
179impl FromStr for Capability {
180 type Err = ();
181
182 fn from_str(s: &str) -> Result<Self, Self::Err> {
183 if let Some(rest) = s.strip_prefix("agent=") {
185 return Ok(Capability::Agent(rest.to_string()));
186 }
187 if let Some(rest) = s.strip_prefix("session-id=") {
188 return Ok(Capability::SessionId(rest.to_string()));
189 }
190 if let Some(rest) = s.strip_prefix("push-cert=") {
191 return Ok(Capability::PushCert(rest.to_string()));
192 }
193 if let Some(rest) = s.strip_prefix("object-format=") {
194 return Ok(Capability::ObjectFormat(rest.to_string()));
195 }
196 if let Some(rest) = s.strip_prefix("filter=") {
197 return Ok(Capability::Filter(rest.to_string()));
198 }
199 if let Some(rest) = s.strip_prefix("symref=") {
200 return Ok(Capability::Symref(rest.to_string()));
201 }
202
203 match s {
204 "multi_ack" => Ok(Capability::MultiAck),
205 "multi_ack_detailed" => Ok(Capability::MultiAckDetailed),
206 "no-done" => Ok(Capability::NoDone),
207 "side-band" => Ok(Capability::SideBand),
208 "side-band-64k" => Ok(Capability::SideBand64k),
209 "report-status" => Ok(Capability::ReportStatus),
210 "report-status-v2" => Ok(Capability::ReportStatusv2),
211 "ofs-delta" => Ok(Capability::OfsDelta),
212 "deepen-since" => Ok(Capability::DeepenSince),
213 "deepen-not" => Ok(Capability::DeepenNot),
214 "deepen-relative" => Ok(Capability::DeepenRelative),
215 "thin-pack" => Ok(Capability::ThinPack),
216 "shallow" => Ok(Capability::Shallow),
217 "include-tag" => Ok(Capability::IncludeTag),
218 "delete-refs" => Ok(Capability::DeleteRefs),
219 "quiet" => Ok(Capability::Quiet),
220 "atomic" => Ok(Capability::Atomic),
221 "no-thin" => Ok(Capability::NoThin),
222 "no-progress" => Ok(Capability::NoProgress),
223 "allow-tip-sha1-in-want" => Ok(Capability::AllowTipSha1InWant),
224 "allow-reachable-sha1-in-want" => Ok(Capability::AllowReachableSha1InWant),
225 "push-options" => Ok(Capability::PushOptions),
226 _ => Ok(Capability::Unknown(s.to_string())),
227 }
228 }
229}
230
231impl std::fmt::Display for Capability {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 match self {
234 Capability::MultiAck => write!(f, "multi_ack"),
235 Capability::MultiAckDetailed => write!(f, "multi_ack_detailed"),
236 Capability::NoDone => write!(f, "no-done"),
237 Capability::SideBand => write!(f, "side-band"),
238 Capability::SideBand64k => write!(f, "side-band-64k"),
239 Capability::ReportStatus => write!(f, "report-status"),
240 Capability::ReportStatusv2 => write!(f, "report-status-v2"),
241 Capability::OfsDelta => write!(f, "ofs-delta"),
242 Capability::DeepenSince => write!(f, "deepen-since"),
243 Capability::DeepenNot => write!(f, "deepen-not"),
244 Capability::DeepenRelative => write!(f, "deepen-relative"),
245 Capability::ThinPack => write!(f, "thin-pack"),
246 Capability::Shallow => write!(f, "shallow"),
247 Capability::IncludeTag => write!(f, "include-tag"),
248 Capability::DeleteRefs => write!(f, "delete-refs"),
249 Capability::Quiet => write!(f, "quiet"),
250 Capability::Atomic => write!(f, "atomic"),
251 Capability::NoThin => write!(f, "no-thin"),
252 Capability::NoProgress => write!(f, "no-progress"),
253 Capability::AllowTipSha1InWant => write!(f, "allow-tip-sha1-in-want"),
254 Capability::AllowReachableSha1InWant => write!(f, "allow-reachable-sha1-in-want"),
255 Capability::PushCert(value) => write!(f, "push-cert={value}"),
256 Capability::PushOptions => write!(f, "push-options"),
257 Capability::ObjectFormat(format) => write!(f, "object-format={format}"),
258 Capability::SessionId(id) => write!(f, "session-id={id}"),
259 Capability::Filter(filter) => write!(f, "filter={filter}"),
260 Capability::Symref(symref) => write!(f, "symref={symref}"),
261 Capability::Agent(agent) => write!(f, "agent={agent}"),
262 Capability::Unknown(s) => write!(f, "{s}"),
263 }
264 }
265}
266
267pub enum SideBand {
269 PackfileData,
271 ProgressInfo,
273 Error,
275}
276
277impl SideBand {
278 pub fn value(&self) -> u8 {
280 match self {
281 Self::PackfileData => b'\x01',
282 Self::ProgressInfo => b'\x02',
283 Self::Error => b'\x03',
284 }
285 }
286}
287
288#[derive(Debug, PartialEq, Clone, Copy)]
290pub enum RefTypeEnum {
291 Branch,
292 Tag,
293}
294
295#[derive(Clone, Debug)]
297pub struct GitRef {
298 pub name: String,
299 pub hash: String,
300}
301
302#[derive(Debug, Clone)]
304pub struct RefCommand {
305 pub old_hash: String,
306 pub new_hash: String,
307 pub ref_name: String,
308 pub ref_type: RefTypeEnum,
309 pub default_branch: bool,
310 pub status: CommandStatus,
311 pub error_message: Option<String>,
312}
313
314#[derive(Debug, Clone)]
316pub enum CommandStatus {
317 Pending,
318 Success,
319 Failed,
320}
321
322impl RefCommand {
323 pub fn new(old_hash: String, new_hash: String, ref_name: String) -> Self {
325 let ref_type = if ref_name.starts_with("refs/tags/") {
327 RefTypeEnum::Tag
328 } else {
329 RefTypeEnum::Branch
330 };
331
332 Self {
333 old_hash,
334 new_hash,
335 ref_name,
336 ref_type,
337 default_branch: false,
338 status: CommandStatus::Pending,
339 error_message: None,
340 }
341 }
342
343 pub fn failed(&mut self, error: String) {
345 self.status = CommandStatus::Failed;
346 self.error_message = Some(error);
347 }
348
349 pub fn success(&mut self) {
351 self.status = CommandStatus::Success;
352 self.error_message = None;
353 }
354
355 pub fn get_status(&self) -> String {
357 match &self.status {
358 CommandStatus::Success => format!("ok {}", self.ref_name),
359 CommandStatus::Failed => {
360 let error = self.error_message.as_deref().unwrap_or("unknown error");
361 format!("ng {} {}", self.ref_name, error)
362 }
363 CommandStatus::Pending => format!("ok {}", self.ref_name), }
365 }
366}
367
368#[derive(Debug, PartialEq, Clone)]
370pub enum CommandType {
371 Create,
372 Update,
373 Delete,
374}
375
376pub const LF: char = '\n';
378pub const SP: char = ' ';
379pub const NUL: char = '\0';
380pub const PKT_LINE_END_MARKER: &[u8; 4] = b"0000";
381
382pub const RECEIVE_CAP_LIST: &str =
384 "report-status report-status-v2 delete-refs quiet atomic no-thin ";
385pub const COMMON_CAP_LIST: &str = "side-band-64k ofs-delta agent=git-internal/0.1.0";
386pub const UPLOAD_CAP_LIST: &str = "multi_ack_detailed no-done include-tag ";
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
394 fn service_type_from_str() {
395 assert_eq!(
396 ServiceType::from_str("git-upload-pack").unwrap(),
397 ServiceType::UploadPack
398 );
399 assert_eq!(
400 ServiceType::from_str("git-receive-pack").unwrap(),
401 ServiceType::ReceivePack
402 );
403 assert!(ServiceType::from_str("git-upload-archive").is_err());
404 }
405
406 #[test]
408 fn capability_round_trip_simple() {
409 for cap in [
410 Capability::SideBand,
411 Capability::SideBand64k,
412 Capability::MultiAck,
413 Capability::ReportStatus,
414 ] {
415 let s = cap.to_string();
416 let parsed = Capability::from_str(&s).expect("should parse");
417 assert_eq!(parsed, cap);
418 }
419 }
420
421 #[test]
423 fn capability_parsing_parameterized() {
424 let agent = Capability::from_str("agent=git/2.41").unwrap();
425 assert_eq!(agent, Capability::Agent("git/2.41".to_string()));
426
427 let fmt = Capability::from_str("object-format=sha256").unwrap();
428 assert_eq!(fmt, Capability::ObjectFormat("sha256".to_string()));
429
430 let unknown = Capability::from_str("custom-cap").unwrap();
431 assert_eq!(unknown, Capability::Unknown("custom-cap".to_string()));
432 }
433
434 #[test]
436 fn sideband_values() {
437 assert_eq!(SideBand::PackfileData.value(), b'\x01');
438 assert_eq!(SideBand::ProgressInfo.value(), b'\x02');
439 assert_eq!(SideBand::Error.value(), b'\x03');
440 }
441
442 #[test]
444 fn ref_command_defaults_and_status() {
445 let mut cmd = RefCommand::new(
446 "old".to_string(),
447 "new".to_string(),
448 "refs/tags/v1.0".to_string(),
449 );
450 assert_eq!(cmd.ref_type, RefTypeEnum::Tag);
451 assert_eq!(cmd.get_status(), "ok refs/tags/v1.0");
452
453 cmd.failed("boom".to_string());
454 assert_eq!(cmd.get_status(), "ng refs/tags/v1.0 boom");
455
456 cmd.success();
457 assert_eq!(cmd.get_status(), "ok refs/tags/v1.0");
458 }
459}