Skip to main content

sley_remote/
receive_pack_server.rs

1//! Full receive-pack server: hooks, proc-receive, ref updates, and status reports.
2
3use std::io::{Cursor, Read, Write};
4use std::path::Path;
5
6use sley_config::GitConfig;
7use sley_core::{GitError, ObjectFormat, Result};
8use sley_odb::{FileObjectDatabase, repository_common_dir};
9use sley_protocol::{
10    ReceivePackCommand, ReceivePackCommandStatus, ReceivePackCommandStatusV2,
11    ReceivePackCommandStatusV2Options, ReceivePackPushRequest, ReceivePackPushRequestHeader,
12    ReceivePackReportStatus, ReceivePackReportStatusV2, ReceivePackUnpackStatus, SideBandChannel,
13    SideBandPacket, validate_receive_pack_push_request_features, write_receive_pack_report_status,
14    write_receive_pack_report_status_v2, write_sideband_packet,
15};
16use sley_refs::FileRefStore;
17
18use crate::local::{apply_receive_pack_ref_transaction, receive_pack_features};
19use crate::proc_receive::{
20    ProcReceiveHookInput, ReceivePackCommandState, apply_proc_receive_hook_failure,
21    mark_proc_receive_commands, parse_proc_receive_refs, run_proc_receive_hook,
22};
23use crate::receive_hooks::{
24    run_post_receive, run_post_update, run_pre_receive, run_push_to_checkout, run_update_hooks,
25};
26
27pub struct ReceivePackServerOptions<'a> {
28    pub quiet: bool,
29    /// When set, traditional receive-pack hook stderr is captured for the push
30    /// client to print as `remote:` lines (in-process local transport).
31    pub remote_stderr: Option<&'a mut Vec<u8>>,
32    /// Run post-receive/post-update after ref updates. The local push client
33    /// runs these itself so hook stdin matches send-pack ordering.
34    pub run_post_hooks: bool,
35}
36
37pub struct ReceivePackServerRequest<'a> {
38    pub git_dir: &'a Path,
39    pub format: ObjectFormat,
40    pub header: &'a ReceivePackPushRequestHeader,
41    pub pack_reader: &'a mut dyn Read,
42    pub config: &'a GitConfig,
43    /// Receive/transfer fsck policy resolved before advertising refs, so
44    /// malformed configuration fails before protocol I/O.
45    pub validation: &'a sley_fsck::FsckPolicy,
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    let mut quarantine = None;
93    if needs_pack_data(&command_states) {
94        let incoming = sley_odb::IncomingPackQuarantine::new(request.git_dir, request.format)?;
95        let incoming_db = incoming.database();
96        match install_pack_from_reader(&incoming_db, request.pack_reader) {
97            Ok(installed) => {
98                let roots = command_states
99                    .iter()
100                    .filter(|state| !state.command.new_id.is_null())
101                    .map(|state| state.command.new_id)
102                    .collect::<Vec<_>>();
103                let validation = validate_receive_objects(
104                    &incoming_db,
105                    request.format,
106                    &roots,
107                    &installed,
108                    request.validation,
109                    remote_stderr,
110                );
111                if request.validation.enabled {
112                    if validation.is_err() {
113                        unpack_error = Some("unpacker error".to_string());
114                    } else {
115                        quarantine = Some(incoming);
116                    }
117                } else if validation.is_err() {
118                    for state in &mut command_states {
119                        if state.error_string.is_none() && !state.command.new_id.is_null() {
120                            state.error_string = Some("missing necessary objects".into());
121                        }
122                    }
123                } else {
124                    quarantine = Some(incoming);
125                }
126            }
127            Err(err) => {
128                let message = err.to_string();
129                unpack_error = Some(message.clone());
130                for state in &mut command_states {
131                    if state.error_string.is_none() {
132                        state.error_string = Some(message.clone());
133                    }
134                }
135            }
136        }
137    }
138
139    let ok_commands: Vec<ReceivePackCommand> = command_states
140        .iter()
141        .filter(|state| state.error_string.is_none())
142        .map(|state| state.command.clone())
143        .collect();
144
145    let quarantine_env = quarantine
146        .as_ref()
147        .map(|q| quarantine_hook_env(request.git_dir, q.object_dir()))
148        .unwrap_or_default();
149
150    if unpack_error.is_none() && !ok_commands.is_empty() {
151        if run_pre_receive(
152            request.git_dir,
153            &ok_commands,
154            push_options,
155            &quarantine_env,
156            remote_stderr,
157            capture_stderr,
158        )
159        .is_err()
160        {
161            for state in &mut command_states {
162                if state.error_string.is_none() {
163                    state.error_string = Some("pre-receive hook declined".into());
164                }
165            }
166        } else if let Some(name) = run_update_hooks(
167            request.git_dir,
168            &ok_commands,
169            &quarantine_env,
170            remote_stderr,
171            capture_stderr,
172        )? {
173            for state in &mut command_states {
174                if state.error_string.is_none() {
175                    if use_atomic {
176                        state.error_string = Some("atomic push failure".into());
177                    } else if state.command.name == name {
178                        state.error_string = Some("hook declined".into());
179                    }
180                }
181            }
182        }
183    }
184
185    if unpack_error.is_none() && run_proc_receive {
186        let output = run_proc_receive_hook(ProcReceiveHookInput {
187            git_dir: request.git_dir,
188            format: request.format,
189            commands: &command_states,
190            push_options,
191            use_atomic,
192            use_push_options: request_uses_push_options(request.header),
193            quarantine_env: &quarantine_env,
194            remote_stderr,
195            capture_stderr,
196        })?;
197        command_states = output.commands;
198        apply_proc_receive_hook_failure(&mut command_states, use_atomic, output.hook_failed);
199    }
200
201    let accepts_incoming_objects = command_states
202        .iter()
203        .any(|state| state.error_string.is_none() && !state.command.new_id.is_null());
204    if unpack_error.is_none()
205        && accepts_incoming_objects
206        && let Some(incoming) = quarantine.take()
207        && let Err(err) = incoming.promote()
208    {
209        let message = err.to_string();
210        unpack_error = Some(message.clone());
211        for state in &mut command_states {
212            if state.error_string.is_none() && !state.command.new_id.is_null() {
213                state.error_string = Some(message.clone());
214            }
215        }
216    }
217
218    if unpack_error.is_none()
219        && let Err(err) = apply_command_updates(request.git_dir, request.format, &command_states)
220    {
221        let message = err.to_string();
222        for state in &mut command_states {
223            if state.error_string.is_none() && !state.defer_ref_update() {
224                state.error_string = Some(message.clone());
225            }
226        }
227    }
228
229    if request.options.run_post_hooks {
230        run_receive_pack_post_hooks(
231            request.git_dir,
232            &command_states,
233            push_options,
234            remote_stderr,
235            capture_stderr,
236        );
237    }
238
239    let report = build_report(&command_states, unpack_error, use_report_v2)?;
240    Ok(ReceivePackServerOutcome {
241        report,
242        command_states,
243    })
244}
245
246pub fn run_receive_pack_post_hooks(
247    git_dir: &Path,
248    command_states: &[ReceivePackCommandState],
249    push_options: &[String],
250    remote_stderr: &mut Vec<u8>,
251    capture_stderr: bool,
252) {
253    let landed: Vec<ReceivePackCommandState> = command_states
254        .iter()
255        .filter(|state| {
256            state.error_string.is_none() && state.command.old_id != state.command.new_id
257        })
258        .cloned()
259        .collect();
260    if landed.is_empty() {
261        return;
262    }
263    let _ = run_post_receive(
264        git_dir,
265        &landed,
266        push_options,
267        remote_stderr,
268        capture_stderr,
269    );
270    let _ = run_post_update(git_dir, &landed, remote_stderr, capture_stderr);
271    let _ = run_push_to_checkout(git_dir, remote_stderr, capture_stderr);
272}
273
274pub fn receive_pack_server_report_v1(report: &ReceivePackServerReport) -> ReceivePackReportStatus {
275    match report {
276        ReceivePackServerReport::V1(status) => status.clone(),
277        ReceivePackServerReport::V2(status) => ReceivePackReportStatus {
278            unpack: status.unpack.clone(),
279            commands: status
280                .commands
281                .iter()
282                .map(|command| match command {
283                    ReceivePackCommandStatusV2::Ok { name, .. } => {
284                        ReceivePackCommandStatus::Ok { name: name.clone() }
285                    }
286                    ReceivePackCommandStatusV2::Ng { name, message } => {
287                        ReceivePackCommandStatus::Ng {
288                            name: name.clone(),
289                            message: message.clone(),
290                        }
291                    }
292                })
293                .collect(),
294        },
295    }
296}
297
298/// Write hook stderr captured during receive-pack as sideband-64k progress packets.
299pub fn write_receive_pack_sideband_stderr(writer: &mut impl Write, stderr: &[u8]) -> Result<()> {
300    if stderr.is_empty() {
301        return Ok(());
302    }
303    for chunk in stderr.chunks(SIDEBAND_PAYLOAD_CHUNK) {
304        write_sideband_packet(
305            writer,
306            &SideBandPacket {
307                channel: SideBandChannel::Progress,
308                data: chunk.to_vec(),
309            },
310        )?;
311    }
312    Ok(())
313}
314
315/// Flush a sideband-64k receive-pack response stream (after progress + report packets).
316pub fn flush_receive_pack_sideband(writer: &mut impl Write) -> Result<()> {
317    writer.write_all(b"0000")?;
318    Ok(())
319}
320
321pub fn write_receive_pack_server_report(
322    writer: &mut impl Write,
323    report: &ReceivePackServerReport,
324    use_sideband: bool,
325    flush_stream: bool,
326) -> Result<()> {
327    match report {
328        ReceivePackServerReport::V1(status) => {
329            if use_sideband {
330                write_report_sideband(
331                    writer,
332                    |buf| write_receive_pack_report_status(buf, status),
333                    flush_stream,
334                )
335            } else {
336                write_receive_pack_report_status(writer, status)
337            }
338        }
339        ReceivePackServerReport::V2(status) => {
340            if use_sideband {
341                write_report_sideband(
342                    writer,
343                    |buf| write_receive_pack_report_status_v2(buf, status),
344                    flush_stream,
345                )
346            } else {
347                write_receive_pack_report_status_v2(writer, status)
348            }
349        }
350    }
351}
352
353pub fn request_uses_sideband(header: &ReceivePackPushRequestHeader) -> bool {
354    header
355        .commands
356        .capabilities
357        .iter()
358        .any(|cap| cap.name == "side-band-64k")
359}
360
361/// Maximum sideband payload bytes per packet (git `LARGE_PACKET_MAX - 5`).
362const SIDEBAND_PAYLOAD_CHUNK: usize = 65_515;
363
364fn write_report_sideband(
365    writer: &mut impl Write,
366    write_report: impl FnOnce(&mut Vec<u8>) -> Result<()>,
367    flush_stream: bool,
368) -> Result<()> {
369    let mut payload = Vec::new();
370    write_report(&mut payload)?;
371    for chunk in payload.chunks(SIDEBAND_PAYLOAD_CHUNK) {
372        write_sideband_packet(
373            writer,
374            &SideBandPacket {
375                channel: SideBandChannel::Data,
376                data: chunk.to_vec(),
377            },
378        )?;
379    }
380    if flush_stream {
381        writer.write_all(b"0000")?;
382    }
383    Ok(())
384}
385
386fn request_uses_atomic(header: &ReceivePackPushRequestHeader) -> bool {
387    header
388        .commands
389        .capabilities
390        .iter()
391        .any(|cap| cap.name == "atomic")
392}
393
394fn request_uses_report_status_v2(header: &ReceivePackPushRequestHeader) -> bool {
395    header
396        .commands
397        .capabilities
398        .iter()
399        .any(|cap| cap.name == "report-status-v2")
400}
401
402fn request_uses_push_options(header: &ReceivePackPushRequestHeader) -> bool {
403    header
404        .commands
405        .capabilities
406        .iter()
407        .any(|cap| cap.name == "push-options")
408}
409
410fn quarantine_hook_env(git_dir: &Path, object_dir: &Path) -> Vec<(String, String)> {
411    let alternate = repository_common_dir(git_dir)
412        .join("objects")
413        .to_string_lossy()
414        .into_owned();
415    let object_dir = object_dir.to_string_lossy().into_owned();
416    vec![
417        ("GIT_OBJECT_DIRECTORY".into(), object_dir.clone()),
418        ("GIT_ALTERNATE_OBJECT_DIRECTORIES".into(), alternate),
419        ("GIT_QUARANTINE_PATH".into(), object_dir),
420    ]
421}
422
423fn needs_pack_data(states: &[ReceivePackCommandState]) -> bool {
424    states
425        .iter()
426        .any(|state| state.error_string.is_none() && !state.command.new_id.is_null())
427}
428
429fn install_pack_from_reader(
430    db: &FileObjectDatabase,
431    reader: &mut dyn Read,
432) -> Result<Vec<sley_core::ObjectId>> {
433    let mut prefix = [0u8; 4];
434    match reader.read_exact(&mut prefix) {
435        Ok(()) => {}
436        Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(Vec::new()),
437        Err(err) => return Err(err.into()),
438    }
439    if &prefix != b"PACK" {
440        return Err(GitError::InvalidFormat(
441            "receive-pack packfile must start with PACK".into(),
442        ));
443    }
444    let mut stream = Cursor::new(prefix).chain(reader);
445    db.install_raw_pack_from_reader(&mut stream)
446        .map(|installed| installed.object_ids)
447}
448
449fn validate_receive_objects(
450    db: &FileObjectDatabase,
451    format: ObjectFormat,
452    roots: &[sley_core::ObjectId],
453    installed: &[sley_core::ObjectId],
454    policy: &sley_fsck::FsckPolicy,
455    remote_stderr: &mut Vec<u8>,
456) -> Result<()> {
457    let report = sley_fsck::fsck_objects_with_options(
458        db,
459        format,
460        roots.iter().copied(),
461        installed.iter().copied(),
462        policy.fsck_options(policy.enabled),
463    );
464    for diagnostic in &policy.diagnostics {
465        remote_stderr.extend_from_slice(diagnostic.as_bytes());
466        remote_stderr.push(b'\n');
467    }
468    for issue in &report.issues {
469        remote_stderr.extend_from_slice(issue.message.as_bytes());
470        remote_stderr.push(b'\n');
471    }
472    if report.is_ok() {
473        Ok(())
474    } else {
475        Err(GitError::Exit(1))
476    }
477}
478
479fn apply_command_updates(
480    git_dir: &Path,
481    format: ObjectFormat,
482    states: &[ReceivePackCommandState],
483) -> Result<()> {
484    let applicable: Vec<ReceivePackCommand> = states
485        .iter()
486        .filter(|state| state.error_string.is_none() && !state.defer_ref_update())
487        .map(|state| state.command.clone())
488        .collect();
489    if applicable.is_empty() {
490        return Ok(());
491    }
492
493    let store = FileRefStore::new(git_dir, format);
494    let db = FileObjectDatabase::from_git_dir(git_dir, format);
495
496    for command in applicable.iter().filter(|c| !c.new_id.is_null()) {
497        if !db.contains(&command.new_id)? {
498            return Err(GitError::InvalidObject(format!(
499                "receive-pack packfile did not provide {}",
500                command.new_id
501            )));
502        }
503    }
504
505    for command in applicable.iter().filter(|c| c.new_id.is_null()) {
506        let current = store.read_ref(&command.name)?;
507        if !command.old_id.is_null() {
508            let Some(sley_refs::RefTarget::Direct(oid)) = current else {
509                return Err(GitError::Transaction(format!(
510                    "expected ref {} to match",
511                    command.name
512                )));
513            };
514            if oid != command.old_id {
515                return Err(GitError::Transaction(format!(
516                    "expected ref {} to match",
517                    command.name
518                )));
519            }
520        }
521    }
522
523    let updates: Vec<ReceivePackCommand> = applicable
524        .iter()
525        .filter(|c| !c.new_id.is_null())
526        .cloned()
527        .collect();
528    apply_receive_pack_ref_transaction(git_dir, format, &store, &updates, &applicable)?;
529    Ok(())
530}
531
532fn build_report(
533    command_states: &[ReceivePackCommandState],
534    unpack_error: Option<String>,
535    use_report_v2: bool,
536) -> Result<ReceivePackServerReport> {
537    let unpack = match unpack_error {
538        None => ReceivePackUnpackStatus::Ok,
539        Some(message) => ReceivePackUnpackStatus::Error(message),
540    };
541    if use_report_v2 {
542        let mut commands = Vec::new();
543        for state in command_states {
544            if let Some(message) = &state.error_string {
545                commands.push(ReceivePackCommandStatusV2::Ng {
546                    name: state.command.name.clone(),
547                    message: message.clone(),
548                });
549                continue;
550            }
551            commands.push(ReceivePackCommandStatusV2::Ok {
552                name: state.command.name.clone(),
553                options: ReceivePackCommandStatusV2Options::default(),
554            });
555            for (index, report) in state.reports.iter().enumerate() {
556                if index > 0 {
557                    commands.push(ReceivePackCommandStatusV2::Ok {
558                        name: state.command.name.clone(),
559                        options: ReceivePackCommandStatusV2Options::default(),
560                    });
561                }
562                if let Some(ReceivePackCommandStatusV2::Ok { options, .. }) = commands.last_mut() {
563                    options.refname = report.refname.clone();
564                    options.old_oid = report.old_oid.clone();
565                    options.new_oid = report.new_oid.clone();
566                    options.forced_update = report.forced_update;
567                }
568            }
569        }
570        Ok(ReceivePackServerReport::V2(ReceivePackReportStatusV2 {
571            unpack,
572            commands,
573        }))
574    } else {
575        let commands = command_states
576            .iter()
577            .map(|state| {
578                if let Some(message) = &state.error_string {
579                    ReceivePackCommandStatus::Ng {
580                        name: state.command.name.clone(),
581                        message: message.clone(),
582                    }
583                } else {
584                    ReceivePackCommandStatus::Ok {
585                        name: state.command.name.clone(),
586                    }
587                }
588            })
589            .collect();
590        Ok(ReceivePackServerReport::V1(ReceivePackReportStatus {
591            unpack,
592            commands,
593        }))
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use sley_object::{EncodedObject, ObjectType, Tree};
601    use sley_pack::PackFile;
602    use sley_refs::RefTarget;
603    use std::fs;
604    use std::time::{SystemTime, UNIX_EPOCH};
605
606    #[test]
607    fn receive_quarantine_discards_rejected_and_promotes_accepted_pack() {
608        let format = ObjectFormat::Sha1;
609        let rejected_repo = temp_repo("receive-quarantine-rejected");
610        let tree = EncodedObject::new(ObjectType::Tree, Tree { entries: vec![] }.write());
611        let tree_oid = tree.object_id(format).expect("tree oid");
612        let malformed = EncodedObject::new(
613            ObjectType::Commit,
614            format!(
615                "tree {tree_oid}\nauthor Bugs Bunny 1 +0000\ncommitter Bugs <bugs@example.invalid> 1 +0000\n\nbad\n"
616            )
617            .into_bytes(),
618        );
619        let malformed_oid = malformed.object_id(format).expect("malformed oid");
620        let rejected_pack =
621            PackFile::write_packed(&[&tree, &malformed], format).expect("rejected pack");
622        let strict_config =
623            GitConfig::parse(b"[receive]\n\tfsckObjects = true\n").expect("strict receive config");
624        let strict_policy = sley_fsck::FsckPolicy::from_config(
625            &strict_config,
626            sley_fsck::FsckConfigKind::Receive,
627            format,
628            &rejected_repo,
629            false,
630        )
631        .expect("strict receive policy");
632        let rejected = serve_test_pack(
633            &rejected_repo,
634            format,
635            malformed_oid,
636            rejected_pack.pack,
637            &strict_config,
638            &strict_policy,
639        );
640        assert!(matches!(
641            rejected.report,
642            ReceivePackServerReport::V1(ReceivePackReportStatus {
643                unpack: ReceivePackUnpackStatus::Error(_),
644                ..
645            })
646        ));
647        let rejected_db = FileObjectDatabase::from_git_dir(&rejected_repo, format);
648        assert!(
649            !rejected_db
650                .contains(&malformed_oid)
651                .expect("rejected object lookup")
652        );
653        assert_eq!(
654            FileRefStore::new(&rejected_repo, format)
655                .read_ref("refs/heads/main")
656                .expect("rejected ref lookup"),
657            None
658        );
659
660        let accepted_repo = temp_repo("receive-quarantine-accepted");
661        let valid = EncodedObject::new(
662            ObjectType::Commit,
663            format!(
664                "tree {tree_oid}\nauthor A <a@example.invalid> 1 +0000\ncommitter A <a@example.invalid> 1 +0000\n\nok\n"
665            )
666            .into_bytes(),
667        );
668        let valid_oid = valid.object_id(format).expect("valid oid");
669        let accepted_pack =
670            PackFile::write_packed(&[&tree, &valid], format).expect("accepted pack");
671        let accepted = serve_test_pack(
672            &accepted_repo,
673            format,
674            valid_oid,
675            accepted_pack.pack,
676            &strict_config,
677            &strict_policy,
678        );
679        assert!(matches!(
680            accepted.report,
681            ReceivePackServerReport::V1(ReceivePackReportStatus {
682                unpack: ReceivePackUnpackStatus::Ok,
683                ..
684            })
685        ));
686        let accepted_db = FileObjectDatabase::from_git_dir(&accepted_repo, format);
687        assert!(
688            accepted_db
689                .contains(&valid_oid)
690                .expect("accepted object lookup")
691        );
692        assert_eq!(
693            FileRefStore::new(&accepted_repo, format)
694                .read_ref("refs/heads/main")
695                .expect("accepted ref lookup"),
696            Some(RefTarget::Direct(valid_oid))
697        );
698    }
699
700    fn serve_test_pack(
701        git_dir: &Path,
702        format: ObjectFormat,
703        new_id: sley_core::ObjectId,
704        pack: Vec<u8>,
705        config: &GitConfig,
706        policy: &sley_fsck::FsckPolicy,
707    ) -> ReceivePackServerOutcome {
708        let header = ReceivePackPushRequestHeader {
709            commands: sley_protocol::ReceivePackRequest {
710                commands: vec![ReceivePackCommand {
711                    old_id: sley_core::ObjectId::null(format),
712                    new_id,
713                    name: "refs/heads/main".into(),
714                }],
715                ..Default::default()
716            },
717            push_options: None,
718        };
719        let mut reader = Cursor::new(pack);
720        serve_receive_pack(ReceivePackServerRequest {
721            git_dir,
722            format,
723            header: &header,
724            pack_reader: &mut reader,
725            config,
726            validation: policy,
727            options: ReceivePackServerOptions {
728                quiet: true,
729                remote_stderr: None,
730                run_post_hooks: false,
731            },
732        })
733        .expect("serve receive pack")
734    }
735
736    fn temp_repo(name: &str) -> std::path::PathBuf {
737        let nonce = SystemTime::now()
738            .duration_since(UNIX_EPOCH)
739            .expect("clock")
740            .as_nanos();
741        let path = std::env::temp_dir().join(format!("sley-{name}-{}-{nonce}", std::process::id()));
742        fs::create_dir_all(path.join("objects/pack")).expect("repo object directory");
743        fs::create_dir_all(path.join("refs/heads")).expect("repo refs directory");
744        fs::write(path.join("HEAD"), b"ref: refs/heads/main\n").expect("repo HEAD");
745        path
746    }
747}