git_internal/protocol/
types.rs

1//! Core enums and error types for the Git smart protocol: service identifiers, transport selection,
2//! capability negotiation, and the shared stream/error aliases used throughout the crate.
3
4use std::{fmt, pin::Pin, str::FromStr};
5
6use bytes::Bytes;
7use futures::stream::Stream;
8
9/// Type alias for protocol data streams to reduce nesting
10pub type ProtocolStream = Pin<Box<dyn Stream<Item = Result<Bytes, ProtocolError>> + Send>>;
11
12/// Protocol error types
13#[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/// Git transport protocol types
59#[derive(Debug, PartialEq, Clone, Copy, Default)]
60pub enum TransportProtocol {
61    Local,
62    #[default]
63    Http,
64    Ssh,
65    Git,
66}
67
68/// Git service types for smart protocol
69#[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/// Git protocol capabilities
97///
98/// ## Implementation Status Overview
99///
100/// ### Implemented capabilities:
101/// - **Data transmission**: SideBand, SideBand64k - Multiplexed data streams via side-band formatter
102/// - **Status reporting**: ReportStatus, ReportStatusv2 - Push status feedback via protocol handlers
103/// - **Pack optimization**: OfsDelta, ThinPack, NoThin - Delta compression and efficient transmission
104/// - **Protocol control**: MultiAckDetailed, NoDone - ACK mechanism optimization for upload-pack
105/// - **Push control**: Atomic, DeleteRefs, Quiet - Atomic operations and reference management
106/// - **Tag handling**: IncludeTag - Automatic tag inclusion for upload-pack
107/// - **Client identification**: Agent - Client/server identification in capability negotiation
108///
109/// ### Not yet implemented capabilities:
110/// - **Basic protocol**: MultiAck - Basic multi-ack support (only detailed version implemented)
111/// - **Shallow cloning**: Shallow, DeepenSince, DeepenNot, DeepenRelative - Depth control for shallow clones
112/// - **Progress control**: NoProgress - Progress output suppression
113/// - **Special fetch**: AllowTipSha1InWant, AllowReachableSha1InWant - SHA1 validation in want processing
114/// - **Security**: PushCert - Push certificate verification mechanism
115/// - **Extensions**: PushOptions, Filter, Symref - Extended parameter handling
116/// - **Session management**: SessionId, ObjectFormat - Session and format negotiation
117#[derive(Debug, Clone, PartialEq)]
118pub enum Capability {
119    /// Multi-ack capability for upload-pack protocol
120    MultiAck,
121    /// Multi-ack-detailed capability for more granular acknowledgment
122    MultiAckDetailed,
123    /// No-done capability to optimize upload-pack protocol
124    NoDone,
125    /// Side-band capability for multiplexing data streams
126    SideBand,
127    /// Side-band-64k capability for larger side-band packets
128    SideBand64k,
129    /// Report-status capability for push status reporting
130    ReportStatus,
131    /// Report-status-v2 capability for enhanced push status reporting
132    ReportStatusv2,
133    /// OFS-delta capability for offset-based delta compression
134    OfsDelta,
135    /// Deepen-since capability for shallow clone with time-based depth
136    DeepenSince,
137    /// Deepen-not capability for shallow clone exclusions
138    DeepenNot,
139    /// Deepen-relative capability for relative depth specification
140    DeepenRelative,
141    /// Thin-pack capability for efficient pack transmission
142    ThinPack,
143    /// Shallow capability for shallow clone support
144    Shallow,
145    /// Include-tag capability for automatic tag inclusion
146    IncludeTag,
147    /// Delete-refs capability for reference deletion
148    DeleteRefs,
149    /// Quiet capability to suppress output
150    Quiet,
151    /// Atomic capability for atomic push operations
152    Atomic,
153    /// No-thin capability to disable thin pack
154    NoThin,
155    /// No-progress capability to disable progress reporting
156    NoProgress,
157    /// Allow-tip-sha1-in-want capability for fetching specific commits
158    AllowTipSha1InWant,
159    /// Allow-reachable-sha1-in-want capability for fetching reachable commits
160    AllowReachableSha1InWant,
161    /// Push-cert capability for signed push certificates
162    PushCert(String),
163    /// Push-options capability for additional push metadata
164    PushOptions,
165    /// Object-format capability for specifying hash algorithm
166    ObjectFormat(String),
167    /// Session-id capability for session tracking
168    SessionId(String),
169    /// Filter capability for partial clone support
170    Filter(String),
171    /// Symref capability for symbolic reference information
172    Symref(String),
173    /// Agent capability for client/server identification
174    Agent(String),
175    /// Unknown capability for forward compatibility
176    Unknown(String),
177}
178
179impl FromStr for Capability {
180    type Err = ();
181
182    fn from_str(s: &str) -> Result<Self, Self::Err> {
183        // Parameterized capabilities
184        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
267/// Side-band types for multiplexed data streams
268pub enum SideBand {
269    /// Sideband 1 contains packfile data
270    PackfileData,
271    /// Sideband 2 contains progress information
272    ProgressInfo,
273    /// Sideband 3 contains error information
274    Error,
275}
276
277impl SideBand {
278    /// Get the byte value associated with the side-band type
279    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/// Reference types in Git
289#[derive(Debug, PartialEq, Clone, Copy)]
290pub enum RefTypeEnum {
291    Branch,
292    Tag,
293}
294
295/// Git reference information
296#[derive(Clone, Debug)]
297pub struct GitRef {
298    pub name: String,
299    pub hash: String,
300}
301
302/// Reference command for push operations
303#[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/// Status of a reference command
315#[derive(Debug, Clone)]
316pub enum CommandStatus {
317    Pending,
318    Success,
319    Failed,
320}
321
322impl RefCommand {
323    /// Create a new reference command
324    pub fn new(old_hash: String, new_hash: String, ref_name: String) -> Self {
325        // Determine ref type based on ref name
326        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    /// Mark the command as failed with an error message
344    pub fn failed(&mut self, error: String) {
345        self.status = CommandStatus::Failed;
346        self.error_message = Some(error);
347    }
348
349    /// Mark the command as successful
350    pub fn success(&mut self) {
351        self.status = CommandStatus::Success;
352        self.error_message = None;
353    }
354
355    /// Get the status string for the command
356    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), // Default to ok for pending
364        }
365    }
366}
367
368/// Command types for reference updates
369#[derive(Debug, PartialEq, Clone)]
370pub enum CommandType {
371    Create,
372    Update,
373    Delete,
374}
375
376/// Protocol constants
377pub const LF: char = '\n';
378pub const SP: char = ' ';
379pub const NUL: char = '\0';
380pub const PKT_LINE_END_MARKER: &[u8; 4] = b"0000";
381
382// Git protocol capability lists
383pub 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    /// ServiceType parsing should accept known services and reject unknown.
393    #[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    /// Capability simple flags should round-trip Display -> FromStr.
407    #[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    /// Parameterized capabilities should preserve their payload strings.
422    #[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    /// SideBand variants should map to the expected byte tags.
435    #[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    /// RefCommand should infer ref type and expose status strings.
443    #[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}