1use std::collections::HashSet;
4use std::io::{Cursor, Read, Write};
5use std::path::Path;
6
7use sley_config::GitConfig;
8use sley_core::{GitError, ObjectFormat, Result};
9use sley_odb::{repository_common_dir, FileObjectDatabase};
10use sley_protocol::{
11 validate_receive_pack_push_request_features, write_receive_pack_report_status,
12 write_receive_pack_report_status_v2, write_sideband_packet, ReceivePackCommand,
13 ReceivePackCommandStatus, ReceivePackCommandStatusV2, ReceivePackCommandStatusV2Options,
14 ReceivePackPushRequest, ReceivePackPushRequestHeader, ReceivePackReportStatus,
15 ReceivePackReportStatusV2, ReceivePackUnpackStatus, SideBandChannel, SideBandPacket,
16};
17use sley_refs::FileRefStore;
18
19use crate::local::{apply_receive_pack_ref_transaction, receive_pack_features};
20use crate::proc_receive::{
21 apply_proc_receive_hook_failure, mark_proc_receive_commands, parse_proc_receive_refs,
22 run_proc_receive_hook, ProcReceiveHookInput, ReceivePackCommandState,
23};
24use crate::push::stage_local_push_quarantine;
25use crate::receive_hooks::{
26 hook_exists, run_post_receive, run_post_update, run_pre_receive, run_push_to_checkout,
27 run_update_hooks,
28};
29
30pub struct ReceivePackServerOptions<'a> {
31 pub quiet: bool,
32 pub remote_stderr: Option<&'a mut Vec<u8>>,
35 pub run_post_hooks: bool,
38}
39
40pub struct ReceivePackServerRequest<'a> {
41 pub git_dir: &'a Path,
42 pub format: ObjectFormat,
43 pub header: &'a ReceivePackPushRequestHeader,
44 pub pack_reader: &'a mut dyn Read,
45 pub config: &'a GitConfig,
46 pub options: ReceivePackServerOptions<'a>,
47}
48
49pub enum ReceivePackServerReport {
50 V1(ReceivePackReportStatus),
51 V2(ReceivePackReportStatusV2),
52}
53
54pub struct ReceivePackServerOutcome {
55 pub report: ReceivePackServerReport,
56 pub command_states: Vec<ReceivePackCommandState>,
57}
58
59pub fn serve_receive_pack(
60 request: ReceivePackServerRequest<'_>,
61) -> Result<ReceivePackServerOutcome> {
62 let mut discard_stderr = Vec::new();
63 let capture_stderr = request.options.remote_stderr.is_some();
64 let remote_stderr = request.options.remote_stderr.unwrap_or(&mut discard_stderr);
65
66 validate_receive_pack_push_request_features(
67 &receive_pack_features(request.format),
68 &ReceivePackPushRequest {
69 commands: request.header.commands.clone(),
70 push_options: request.header.push_options.clone(),
71 packfile: Vec::new(),
72 },
73 )?;
74
75 let use_atomic = request_uses_atomic(request.header);
76 let use_report_v2 = request_uses_report_status_v2(request.header);
77 let push_options = request.header.push_options.as_deref().unwrap_or(&[]);
78
79 let mut command_states: Vec<ReceivePackCommandState> = request
80 .header
81 .commands
82 .commands
83 .iter()
84 .cloned()
85 .map(ReceivePackCommandState::new)
86 .collect();
87
88 let proc_patterns = parse_proc_receive_refs(request.config);
89 let run_proc_receive = mark_proc_receive_commands(&proc_patterns, &mut command_states);
90
91 let mut unpack_error = None;
92 if needs_pack_data(&command_states) {
93 match install_pack_from_reader(request.git_dir, request.format, request.pack_reader) {
94 Ok(()) => {}
95 Err(err) => {
96 let message = err.to_string();
97 unpack_error = Some(message.clone());
98 for state in &mut command_states {
99 if state.error_string.is_none() {
100 state.error_string = Some(message.clone());
101 }
102 }
103 }
104 }
105 }
106
107 let ok_commands: Vec<ReceivePackCommand> = command_states
108 .iter()
109 .filter(|state| state.error_string.is_none())
110 .map(|state| state.command.clone())
111 .collect();
112
113 let quarantine = if unpack_error.is_none()
114 && !ok_commands.is_empty()
115 && receive_pre_hooks_may_run(request.git_dir)
116 {
117 let local_db = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
118 let common_git_dir = repository_common_dir(request.git_dir);
119 stage_local_push_quarantine(
120 request.git_dir,
121 &common_git_dir,
122 request.format,
123 &local_db,
124 &ok_commands,
125 )?
126 } else {
127 None
128 };
129 let quarantine_env = quarantine
130 .as_ref()
131 .map(|q| quarantine_hook_env(request.git_dir, q.object_dir()))
132 .unwrap_or_default();
133
134 if unpack_error.is_none() && !ok_commands.is_empty() {
135 if run_pre_receive(
136 request.git_dir,
137 &ok_commands,
138 push_options,
139 &quarantine_env,
140 remote_stderr,
141 capture_stderr,
142 )
143 .is_err()
144 {
145 for state in &mut command_states {
146 if state.error_string.is_none() {
147 state.error_string = Some("pre-receive hook declined".into());
148 }
149 }
150 } else if let Some(name) = run_update_hooks(
151 request.git_dir,
152 &ok_commands,
153 &quarantine_env,
154 remote_stderr,
155 capture_stderr,
156 )? {
157 for state in &mut command_states {
158 if state.error_string.is_none() {
159 if use_atomic {
160 state.error_string = Some("atomic push failure".into());
161 } else if state.command.name == name {
162 state.error_string = Some("hook declined".into());
163 }
164 }
165 }
166 }
167 }
168
169 if unpack_error.is_none() && run_proc_receive {
170 let output = run_proc_receive_hook(ProcReceiveHookInput {
171 git_dir: request.git_dir,
172 format: request.format,
173 commands: &command_states,
174 push_options,
175 use_atomic,
176 use_push_options: request_uses_push_options(request.header),
177 remote_stderr,
178 capture_stderr,
179 })?;
180 command_states = output.commands;
181 apply_proc_receive_hook_failure(&mut command_states, use_atomic, output.hook_failed);
182 }
183
184 if unpack_error.is_none() {
185 if let Err(err) = apply_command_updates(request.git_dir, request.format, &command_states) {
186 let message = err.to_string();
187 for state in &mut command_states {
188 if state.error_string.is_none() && !state.defer_ref_update() {
189 state.error_string = Some(message.clone());
190 }
191 }
192 }
193 }
194
195 if request.options.run_post_hooks {
196 run_receive_pack_post_hooks(
197 request.git_dir,
198 &command_states,
199 push_options,
200 remote_stderr,
201 capture_stderr,
202 );
203 }
204
205 let report = build_report(&command_states, unpack_error, use_report_v2)?;
206 Ok(ReceivePackServerOutcome {
207 report,
208 command_states,
209 })
210}
211
212pub fn run_receive_pack_post_hooks(
213 git_dir: &Path,
214 command_states: &[ReceivePackCommandState],
215 push_options: &[String],
216 remote_stderr: &mut Vec<u8>,
217 capture_stderr: bool,
218) {
219 let landed: Vec<ReceivePackCommandState> = command_states
220 .iter()
221 .filter(|state| {
222 state.error_string.is_none() && state.command.old_id != state.command.new_id
223 })
224 .cloned()
225 .collect();
226 if landed.is_empty() {
227 return;
228 }
229 let _ = run_post_receive(
230 git_dir,
231 &landed,
232 push_options,
233 remote_stderr,
234 capture_stderr,
235 );
236 let _ = run_post_update(git_dir, &landed, remote_stderr, capture_stderr);
237 let _ = run_push_to_checkout(git_dir, remote_stderr, capture_stderr);
238}
239
240pub fn receive_pack_server_report_v1(report: &ReceivePackServerReport) -> ReceivePackReportStatus {
241 match report {
242 ReceivePackServerReport::V1(status) => status.clone(),
243 ReceivePackServerReport::V2(status) => ReceivePackReportStatus {
244 unpack: status.unpack.clone(),
245 commands: status
246 .commands
247 .iter()
248 .map(|command| match command {
249 ReceivePackCommandStatusV2::Ok { name, .. } => {
250 ReceivePackCommandStatus::Ok { name: name.clone() }
251 }
252 ReceivePackCommandStatusV2::Ng { name, message } => {
253 ReceivePackCommandStatus::Ng {
254 name: name.clone(),
255 message: message.clone(),
256 }
257 }
258 })
259 .collect(),
260 },
261 }
262}
263
264pub fn write_receive_pack_sideband_stderr(
266 writer: &mut impl Write,
267 stderr: &[u8],
268) -> Result<()> {
269 if stderr.is_empty() {
270 return Ok(());
271 }
272 for chunk in stderr.chunks(SIDEBAND_PAYLOAD_CHUNK) {
273 write_sideband_packet(
274 writer,
275 &SideBandPacket {
276 channel: SideBandChannel::Progress,
277 data: chunk.to_vec(),
278 },
279 )?;
280 }
281 Ok(())
282}
283
284pub fn flush_receive_pack_sideband(writer: &mut impl Write) -> Result<()> {
286 writer.write_all(b"0000")?;
287 Ok(())
288}
289
290pub fn write_receive_pack_server_report(
291 writer: &mut impl Write,
292 report: &ReceivePackServerReport,
293 use_sideband: bool,
294 flush_stream: bool,
295) -> Result<()> {
296 match report {
297 ReceivePackServerReport::V1(status) => {
298 if use_sideband {
299 write_report_sideband(
300 writer,
301 |buf| write_receive_pack_report_status(buf, status),
302 flush_stream,
303 )
304 } else {
305 write_receive_pack_report_status(writer, status)
306 }
307 }
308 ReceivePackServerReport::V2(status) => {
309 if use_sideband {
310 write_report_sideband(
311 writer,
312 |buf| write_receive_pack_report_status_v2(buf, status),
313 flush_stream,
314 )
315 } else {
316 write_receive_pack_report_status_v2(writer, status)
317 }
318 }
319 }
320}
321
322pub fn request_uses_sideband(header: &ReceivePackPushRequestHeader) -> bool {
323 header
324 .commands
325 .capabilities
326 .iter()
327 .any(|cap| cap.name == "side-band-64k")
328}
329
330const SIDEBAND_PAYLOAD_CHUNK: usize = 65_515;
332
333fn write_report_sideband(
334 writer: &mut impl Write,
335 write_report: impl FnOnce(&mut Vec<u8>) -> Result<()>,
336 flush_stream: bool,
337) -> Result<()> {
338 let mut payload = Vec::new();
339 write_report(&mut payload)?;
340 for chunk in payload.chunks(SIDEBAND_PAYLOAD_CHUNK) {
341 write_sideband_packet(
342 writer,
343 &SideBandPacket {
344 channel: SideBandChannel::Data,
345 data: chunk.to_vec(),
346 },
347 )?;
348 }
349 if flush_stream {
350 writer.write_all(b"0000")?;
351 }
352 Ok(())
353}
354
355fn request_uses_atomic(header: &ReceivePackPushRequestHeader) -> bool {
356 header
357 .commands
358 .capabilities
359 .iter()
360 .any(|cap| cap.name == "atomic")
361}
362
363fn request_uses_report_status_v2(header: &ReceivePackPushRequestHeader) -> bool {
364 header
365 .commands
366 .capabilities
367 .iter()
368 .any(|cap| cap.name == "report-status-v2")
369}
370
371fn request_uses_push_options(header: &ReceivePackPushRequestHeader) -> bool {
372 header
373 .commands
374 .capabilities
375 .iter()
376 .any(|cap| cap.name == "push-options")
377}
378
379fn receive_pre_hooks_may_run(git_dir: &Path) -> bool {
380 hook_exists(git_dir, "pre-receive") || hook_exists(git_dir, "update")
381}
382
383fn quarantine_hook_env(git_dir: &Path, object_dir: &Path) -> Vec<(String, String)> {
384 let alternate = repository_common_dir(git_dir)
385 .join("objects")
386 .to_string_lossy()
387 .into_owned();
388 let object_dir = object_dir.to_string_lossy().into_owned();
389 vec![
390 ("GIT_OBJECT_DIRECTORY".into(), object_dir.clone()),
391 ("GIT_ALTERNATE_OBJECT_DIRECTORIES".into(), alternate),
392 ("GIT_QUARANTINE_PATH".into(), object_dir),
393 ]
394}
395
396fn needs_pack_data(states: &[ReceivePackCommandState]) -> bool {
397 states
398 .iter()
399 .any(|state| state.error_string.is_none() && !state.command.new_id.is_null())
400}
401
402fn install_pack_from_reader(
403 git_dir: &Path,
404 format: ObjectFormat,
405 reader: &mut dyn Read,
406) -> Result<()> {
407 let mut prefix = [0u8; 4];
408 match reader.read_exact(&mut prefix) {
409 Ok(()) => {}
410 Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(()),
411 Err(err) => return Err(err.into()),
412 }
413 if &prefix != b"PACK" {
414 return Err(GitError::InvalidFormat(
415 "receive-pack packfile must start with PACK".into(),
416 ));
417 }
418 let db = FileObjectDatabase::from_git_dir(git_dir, format);
419 let mut stream = Cursor::new(prefix).chain(reader);
420 db.install_raw_pack_from_reader(&mut stream).map(|_| ())
421}
422
423fn apply_command_updates(
424 git_dir: &Path,
425 format: ObjectFormat,
426 states: &[ReceivePackCommandState],
427) -> Result<()> {
428 let applicable: Vec<ReceivePackCommand> = states
429 .iter()
430 .filter(|state| state.error_string.is_none() && !state.defer_ref_update())
431 .map(|state| state.command.clone())
432 .collect();
433 if applicable.is_empty() {
434 return Ok(());
435 }
436
437 let store = FileRefStore::new(git_dir, format);
438 let db = FileObjectDatabase::from_git_dir(git_dir, format);
439
440 for command in applicable.iter().filter(|c| !c.new_id.is_null()) {
441 if !db.contains(&command.new_id)? {
442 return Err(GitError::InvalidObject(format!(
443 "receive-pack packfile did not provide {}",
444 command.new_id
445 )));
446 }
447 }
448
449 for command in applicable.iter().filter(|c| c.new_id.is_null()) {
450 let current = store.read_ref(&command.name)?;
451 if !command.old_id.is_null() {
452 let Some(sley_refs::RefTarget::Direct(oid)) = current else {
453 return Err(GitError::Transaction(format!(
454 "expected ref {} to match",
455 command.name
456 )));
457 };
458 if oid != command.old_id {
459 return Err(GitError::Transaction(format!(
460 "expected ref {} to match",
461 command.name
462 )));
463 }
464 }
465 }
466
467 let updates: Vec<ReceivePackCommand> = applicable
468 .iter()
469 .filter(|c| !c.new_id.is_null())
470 .cloned()
471 .collect();
472 apply_receive_pack_ref_transaction(git_dir, format, &store, &updates, &applicable)?;
473 Ok(())
474}
475
476fn build_report(
477 command_states: &[ReceivePackCommandState],
478 unpack_error: Option<String>,
479 use_report_v2: bool,
480) -> Result<ReceivePackServerReport> {
481 let unpack = match unpack_error {
482 None => ReceivePackUnpackStatus::Ok,
483 Some(message) => ReceivePackUnpackStatus::Error(message),
484 };
485 if use_report_v2 {
486 let mut commands = Vec::new();
487 for state in command_states {
488 if let Some(message) = &state.error_string {
489 commands.push(ReceivePackCommandStatusV2::Ng {
490 name: state.command.name.clone(),
491 message: message.clone(),
492 });
493 continue;
494 }
495 commands.push(ReceivePackCommandStatusV2::Ok {
496 name: state.command.name.clone(),
497 options: ReceivePackCommandStatusV2Options::default(),
498 });
499 for (index, report) in state.reports.iter().enumerate() {
500 if index > 0 {
501 commands.push(ReceivePackCommandStatusV2::Ok {
502 name: state.command.name.clone(),
503 options: ReceivePackCommandStatusV2Options::default(),
504 });
505 }
506 if let Some(ReceivePackCommandStatusV2::Ok { options, .. }) = commands.last_mut() {
507 options.refname = report.refname.clone();
508 options.old_oid = report.old_oid.clone();
509 options.new_oid = report.new_oid.clone();
510 options.forced_update = report.forced_update;
511 }
512 }
513 }
514 Ok(ReceivePackServerReport::V2(ReceivePackReportStatusV2 {
515 unpack,
516 commands,
517 }))
518 } else {
519 let commands = command_states
520 .iter()
521 .map(|state| {
522 if let Some(message) = &state.error_string {
523 ReceivePackCommandStatus::Ng {
524 name: state.command.name.clone(),
525 message: message.clone(),
526 }
527 } else {
528 ReceivePackCommandStatus::Ok {
529 name: state.command.name.clone(),
530 }
531 }
532 })
533 .collect();
534 Ok(ReceivePackServerReport::V1(ReceivePackReportStatus {
535 unpack,
536 commands,
537 }))
538 }
539}