gunnar_sendpack/
command.rs1use std::collections::HashSet;
19
20use bstr::{BString, ByteSlice};
21use gix_hash::{Kind, ObjectId};
22
23use crate::capabilities;
24use crate::error::{Error, Result};
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct PushCommand {
29 pub name: BString,
31 pub old: ObjectId,
34 pub new: ObjectId,
36}
37
38impl PushCommand {
39 pub fn update(name: impl Into<BString>, old: ObjectId, new: ObjectId) -> Self {
41 PushCommand {
42 name: name.into(),
43 old,
44 new,
45 }
46 }
47
48 pub fn create(name: impl Into<BString>, new: ObjectId) -> Self {
50 PushCommand {
51 name: name.into(),
52 old: ObjectId::null(new.kind()),
53 new,
54 }
55 }
56
57 pub fn delete(name: impl Into<BString>, old: ObjectId) -> Self {
59 PushCommand {
60 name: name.into(),
61 new: ObjectId::null(old.kind()),
62 old,
63 }
64 }
65
66 pub fn is_delete(&self) -> bool {
68 self.new.is_null()
69 }
70
71 pub fn is_create(&self) -> bool {
73 self.old.is_null()
74 }
75
76 pub fn line(&self) -> Vec<u8> {
79 let mut out = format!("{} {} ", self.old.to_hex(), self.new.to_hex()).into_bytes();
80 out.extend_from_slice(self.name.as_slice());
81 out
82 }
83}
84
85pub const SHALLOW_PREFIX: &[u8] = b"shallow ";
88
89pub fn parse_line(line: &[u8], hash_kind: Kind) -> Result<PushCommand> {
97 let mut parts = line.splitn(3, |&b| b == b' ');
98 let (Some(old), Some(new), Some(name)) = (parts.next(), parts.next(), parts.next()) else {
99 return Err(Error::protocol(format!(
100 "{:?} is not an `<old> <new> <ref>` command",
101 line.as_bstr()
102 )));
103 };
104 let parse = |hex: &[u8]| -> Result<ObjectId> {
105 let id = ObjectId::from_hex(hex).map_err(|err| {
106 Error::protocol(format!("{:?} is not an object id: {err}", hex.as_bstr()))
107 })?;
108 if id.kind() != hash_kind {
109 return Err(Error::ObjectFormat {
110 ours: crate::advertisement::object_format_name(hash_kind).unwrap_or("unsupported"),
111 theirs: crate::advertisement::object_format_name(id.kind())
112 .unwrap_or("unsupported"),
113 });
114 }
115 Ok(id)
116 };
117 let (old, new) = (parse(old)?, parse(new)?);
118 if old.is_null() && new.is_null() {
119 return Err(Error::protocol(format!(
120 "{:?} has a null old AND a null new oid, which is neither a create, \
121 an update nor a delete",
122 name.as_bstr()
123 )));
124 }
125 Ok(PushCommand {
126 name: name.into(),
127 old,
128 new,
129 })
130}
131
132pub fn lines(commands: &[PushCommand], caps: &[String]) -> Result<Vec<Vec<u8>>> {
145 if commands.is_empty() {
146 return Err(Error::invalid(
147 "a push with no commands has nothing to say; send nothing instead",
148 ));
149 }
150 let mut seen: HashSet<&[u8]> = HashSet::new();
151 for c in commands {
152 if !seen.insert(c.name.as_slice()) {
153 return Err(Error::invalid(format!(
154 "the command list names {:?} twice; the remote would apply one of \
155 them and the client could not say which",
156 c.name.as_bstr()
157 )));
158 }
159 if c.is_delete() && c.is_create() {
160 return Err(Error::invalid(format!(
161 "{:?} has a null old AND a null new oid, which is neither a create, \
162 an update nor a delete",
163 c.name.as_bstr()
164 )));
165 }
166 if c.name.contains(&b'\n') || c.name.contains(&0) {
167 return Err(Error::invalid(format!(
168 "the reference name {:?} contains a newline or a NUL, either of \
169 which would be read as the end of the line",
170 c.name.as_bstr()
171 )));
172 }
173 }
174
175 let mut out = Vec::with_capacity(commands.len());
176 for (i, c) in commands.iter().enumerate() {
177 let mut line = c.line();
178 if i == 0 {
179 capabilities::attach(&mut line, caps);
180 }
181 out.push(line);
182 }
183 Ok(out)
184}
185
186pub fn push_option_lines(options: &[String]) -> Result<Vec<Vec<u8>>> {
188 options
189 .iter()
190 .map(|opt| {
191 if opt.contains('\n') {
192 return Err(Error::invalid(format!(
193 "push option {opt:?} contains a newline, which the wire cannot carry"
194 )));
195 }
196 Ok(opt.as_bytes().to_vec())
197 })
198 .collect()
199}