1use std::collections::BTreeMap;
13
14pub const FEATURE_GIT: u32 = 1 << 7;
16
17pub const C2S_GIT_OPEN: u8 = 0xA0;
27pub const C2S_GIT_CLOSE: u8 = 0xA1;
29pub const C2S_GIT_ACK: u8 = 0xA2;
31pub const C2S_GIT_LOG: u8 = 0xA7;
33pub const C2S_GIT_TREE: u8 = 0xAB;
35pub const C2S_GIT_BLOB: u8 = 0xAC;
37pub const C2S_GIT_DIFF: u8 = 0xAD;
39pub const C2S_GIT_PATCH: u8 = 0xAE;
41pub const C2S_GIT_INDEX: u8 = 0xAF;
43pub const C2S_GIT_CANCEL: u8 = 0xA3;
45pub const C2S_GIT_BASE: u8 = 0xB0;
47pub const C2S_GIT_RESOLVE: u8 = 0xA6;
52pub const C2S_GIT_LOG_WATCH: u8 = 0xA8;
58pub const C2S_GIT_LOG_UNWATCH: u8 = 0xA9;
60pub const C2S_GIT_LOG_ACK: u8 = 0xAA;
62
63pub const C2S_GIT_DISCOVER: u8 = 0xB1;
67pub const C2S_GIT_BLAME: u8 = 0xB2;
69pub const C2S_GIT_REFLOG: u8 = 0xB3;
72pub const C2S_GIT_FETCH: u8 = 0xB4;
74
75pub const S2C_GIT_REPO: u8 = 0xA0;
81pub const S2C_GIT_STATE: u8 = 0xA4;
83pub const S2C_GIT_CLOSED: u8 = 0xA5;
85pub const S2C_GIT_COMMITS: u8 = 0xA7;
87pub const S2C_GIT_TREE: u8 = 0xAB;
89pub const S2C_GIT_BLOB: u8 = 0xAC;
92pub const S2C_GIT_DIFF: u8 = 0xAD;
94pub const S2C_GIT_PATCH: u8 = 0xAE;
97pub const S2C_GIT_INDEX: u8 = 0xAF;
99pub const S2C_GIT_BASE: u8 = 0xB0;
101pub const S2C_GIT_RESOLVE: u8 = 0xA6;
103pub const S2C_GIT_LOG_PAGE: u8 = 0xA8;
109
110pub const S2C_GIT_DISCOVER: u8 = 0xB1;
112pub const S2C_GIT_BLAME: u8 = 0xB2;
114pub const S2C_GIT_REFLOG: u8 = 0xB3;
116pub const S2C_GIT_FETCH: u8 = 0xB4;
118
119pub const GIT_STATUS_OK: u8 = 0;
122pub const GIT_STATUS_UNKNOWN_ID: u8 = 1;
124pub const GIT_STATUS_NOT_FOUND: u8 = 2;
126pub const GIT_STATUS_WRONG_TYPE: u8 = 3;
128pub const GIT_STATUS_PERMISSION: u8 = 4;
129pub const GIT_STATUS_TOO_LARGE: u8 = 5;
131pub const GIT_STATUS_BUDGET: u8 = 6;
133pub const GIT_STATUS_INVALID: u8 = 7;
135pub const GIT_STATUS_CANCELLED: u8 = 8;
137pub const GIT_STATUS_OTHER: u8 = 9;
139pub const GIT_STATUS_CONFLICT: u8 = 11;
143pub const GIT_STATUS_NO_MERGE_BASE: u8 = 12;
147
148pub fn git_status_text(status: u8) -> &'static str {
152 match status {
153 GIT_STATUS_OK => "ok",
154 GIT_STATUS_UNKNOWN_ID => "unknown repo",
155 GIT_STATUS_NOT_FOUND => "not found",
156 GIT_STATUS_WRONG_TYPE => "wrong object type",
157 GIT_STATUS_PERMISSION => "permission denied",
158 GIT_STATUS_TOO_LARGE => "too large",
159 GIT_STATUS_BUDGET => "budget exhausted",
160 GIT_STATUS_INVALID => "invalid request",
161 GIT_STATUS_CANCELLED => "cancelled",
162 GIT_STATUS_OTHER => "backend error",
163 GIT_STATUS_CONFLICT => "conflict",
164 GIT_STATUS_NO_MERGE_BASE => "no merge base",
165 _ => "unknown status",
166 }
167}
168
169pub const GIT_OPEN_WATCH: u16 = 1 << 0;
173pub const GIT_OPEN_STATUS: u16 = 1 << 1;
175pub const GIT_OPEN_UNTRACKED: u16 = 1 << 2;
177pub const GIT_OPEN_IGNORED: u16 = 1 << 3;
179pub const GIT_OPEN_TRACKING: u16 = 1 << 4;
181pub const GIT_OPEN_REMOTES: u16 = 1 << 5;
184pub const GIT_OPEN_KNOWN: u16 = GIT_OPEN_WATCH
186 | GIT_OPEN_STATUS
187 | GIT_OPEN_UNTRACKED
188 | GIT_OPEN_IGNORED
189 | GIT_OPEN_TRACKING
190 | GIT_OPEN_REMOTES;
191
192pub const GIT_REPO_ID_INVALID: u16 = 0xFFFF;
195pub const GIT_OPEN_NO_CONTEXT: u16 = 0xFFFF;
197
198pub const GIT_OID_FORMAT_SHA1: u8 = 0;
202pub const GIT_OID_FORMAT_SHA256: u8 = 1;
204
205pub const GIT_REPO_BARE: u8 = 1 << 0;
207pub const GIT_REPO_SHALLOW: u8 = 1 << 1;
208pub const GIT_REPO_SPARSE: u8 = 1 << 2;
210pub const GIT_REPO_LINKED: u8 = 1 << 3;
212pub const GIT_REPO_WRITABLE: u8 = 1 << 4;
215pub const GIT_REPO_FETCHABLE: u8 = 1 << 5;
220
221pub const GIT_CLOSED_CLIENT_REQUEST: u8 = 0;
223pub const GIT_CLOSED_REPO_GONE: u8 = 1;
224pub const GIT_CLOSED_PERMISSION_LOST: u8 = 2;
225pub const GIT_CLOSED_BACKEND_FAILED: u8 = 3;
226pub const GIT_CLOSED_RESOURCE_LIMIT: u8 = 4;
227
228pub const GIT_STATE_REFS_TRUNCATED: u8 = 1 << 0;
230pub const GIT_STATE_STATUS_TRUNCATED: u8 = 1 << 1;
231pub const GIT_STATE_PARTIAL: u8 = 1 << 2;
237
238pub const GIT_LOG_FIRST_PARENT: u8 = 1 << 0;
240pub const GIT_LOG_TOPO: u8 = 1 << 1;
242pub const GIT_LOG_FULL_MESSAGE: u8 = 1 << 2;
244pub const GIT_LOG_FOLLOW: u8 = 1 << 3;
246pub const GIT_LOG_PATH_OIDS: u8 = 1 << 4;
248
249pub const GIT_COMMITS_MORE: u8 = 1 << 0;
252
253pub const GIT_BLOB_WHOLE: u8 = 1 << 0;
259
260pub const GIT_DIFF_RENAMES: u8 = 1 << 0;
264pub const GIT_DIFF_UNTRACKED: u8 = 1 << 1;
266pub const GIT_DIFF_IGNORED: u8 = 1 << 2;
267pub const GIT_DIFF_IGNORE_SPACE_CHANGE: u8 = 1 << 3;
269pub const GIT_DIFF_IGNORE_ALL_SPACE: u8 = 1 << 4;
271pub const GIT_DIFF_RAW: u8 = 1 << 5;
274
275pub const GIT_PATCH_RENAMES: u16 = GIT_DIFF_RENAMES as u16;
278pub const GIT_PATCH_UNTRACKED: u16 = GIT_DIFF_UNTRACKED as u16;
279pub const GIT_PATCH_IGNORED: u16 = GIT_DIFF_IGNORED as u16;
280pub const GIT_PATCH_IGNORE_SPACE_CHANGE: u16 = GIT_DIFF_IGNORE_SPACE_CHANGE as u16;
281pub const GIT_PATCH_IGNORE_ALL_SPACE: u16 = GIT_DIFF_IGNORE_ALL_SPACE as u16;
282pub const GIT_PATCH_RAW: u16 = GIT_DIFF_RAW as u16;
283pub const GIT_PATCH_TEXT: u16 = 1 << 6;
285pub const GIT_PATCH_CHAR_SPANS: u16 = 1 << 7;
287pub const GIT_PATCH_NO_SPANS: u16 = 1 << 8;
289pub const GIT_PATCH_BINARY: u16 = 1 << 9;
296
297pub const GIT_RENAME_MAX: u8 = 100;
300
301pub const GIT_TREE_TRUNCATED: u8 = 1 << 0;
306pub const GIT_DIFF_TRUNCATED: u8 = 1 << 0;
307pub const GIT_DIFF_RENAME_LIMIT: u8 = 1 << 1;
312pub const GIT_INDEX_TRUNCATED: u8 = 1 << 0;
313pub const GIT_PATCH_STRUCTURED: u8 = 1 << 0;
315pub const GIT_PATCH_TRUNCATED: u8 = 1 << 1;
316pub const GIT_DISCOVER_TRUNCATED: u8 = 1 << 0;
317pub const GIT_BLAME_TRUNCATED: u8 = 1 << 0;
318pub const GIT_REFLOG_TRUNCATED: u8 = 1 << 0;
319
320pub const GIT_DISCOVER_NESTED: u8 = 1 << 0;
324pub const GIT_DISCOVER_BARE: u8 = 1 << 1;
326
327pub const GIT_BLAME_FOLLOW_RENAMES: u8 = 1 << 0;
330pub const GIT_BLAME_FOLLOW_COPIES: u8 = 1 << 1;
332
333pub const GIT_REFLOG_OLDEST_FIRST: u8 = 1 << 0;
336
337pub const GIT_FETCH_PRUNE: u8 = 1 << 0;
339pub const GIT_FETCH_NO_TAGS: u8 = 1 << 1;
340pub const GIT_FETCH_ANCHOR: u8 = 1 << 2;
343
344pub const GIT_ENDPOINT_EMPTY: u8 = 0;
346pub const GIT_ENDPOINT_COMMIT: u8 = 1;
347pub const GIT_ENDPOINT_TREE: u8 = 2;
348pub const GIT_ENDPOINT_INDEX: u8 = 3;
349pub const GIT_ENDPOINT_WORKTREE: u8 = 4;
350pub const GIT_ENDPOINT_MERGE_BASE: u8 = 5;
353
354pub const GIT_RECORD_CURSOR: u8 = 0x7F;
358
359pub const GIT_STATE_RECORD_HEAD: u8 = 0x01;
361pub const GIT_STATE_RECORD_REF: u8 = 0x02;
362pub const GIT_STATE_RECORD_OP: u8 = 0x03;
363pub const GIT_STATE_RECORD_STATUS: u8 = 0x04;
364pub const GIT_STATE_RECORD_UPSTREAM: u8 = 0x05;
365pub const GIT_STATE_RECORD_STASH: u8 = 0x06;
366pub const GIT_STATE_RECORD_REMOTE: u8 = 0x07;
367
368pub const GIT_REMOTE_DEFAULT: u8 = 1 << 0;
371
372pub const GIT_HEAD_DETACHED: u8 = 1 << 0;
374pub const GIT_HEAD_UNBORN: u8 = 1 << 1;
375
376pub const GIT_REF_PEELED_VALID: u8 = 1 << 0;
379pub const GIT_REF_SYMBOLIC: u8 = 1 << 1;
380
381pub const GIT_OP_MERGE: u8 = 1;
383pub const GIT_OP_REBASE: u8 = 2;
384pub const GIT_OP_CHERRY_PICK: u8 = 3;
385pub const GIT_OP_REVERT: u8 = 4;
386pub const GIT_OP_BISECT: u8 = 5;
387
388pub const GIT_STATUS_ENTRY_CONFLICTED: u8 = 1 << 0;
390
391pub const GIT_UPSTREAM_GONE: u8 = 1 << 0;
394pub const GIT_UPSTREAM_COUNTS_VALID: u8 = 1 << 1;
396
397pub const GIT_COMMIT_RECORD_COMMIT: u8 = 0x01;
399pub const GIT_COMMIT_RECORD_PATH_AT: u8 = 0x02;
400
401pub const GIT_COMMIT_LOSSY_ENCODING: u8 = 1 << 0;
404
405pub const GIT_TREE_RECORD_ENTRY: u8 = 0x02;
407
408pub const GIT_OTYPE_COMMIT: u8 = 1;
411pub const GIT_OTYPE_TREE: u8 = 2;
412pub const GIT_OTYPE_BLOB: u8 = 3;
413
414pub const GIT_DIFF_RECORD_ENTRY: u8 = 0x03;
416pub const GIT_DIFF_RECORD_BASE: u8 = 0x04;
417
418pub const GIT_DIFF_ENTRY_BINARY: u8 = 1 << 0;
420pub const GIT_DIFF_ENTRY_SUBMODULE: u8 = 1 << 1;
421pub const GIT_DIFF_ENTRY_FILTERED: u8 = 1 << 2;
426
427pub const GIT_PATCH_RECORD_FILE: u8 = 0x01;
429pub const GIT_PATCH_RECORD_ROW: u8 = 0x02;
430pub const GIT_PATCH_RECORD_GAP: u8 = 0x03;
431pub const GIT_PATCH_RECORD_BASE: u8 = 0x04;
432
433pub const GIT_PATCH_FILE_BINARY: u8 = 1 << 0;
436pub const GIT_PATCH_FILE_FILTERED: u8 = 1 << 1;
438
439pub const GIT_INDEX_RECORD_ENTRY: u8 = 0x04;
441
442pub const GIT_DISCOVER_RECORD_REPO: u8 = 0x01;
444pub const GIT_BLAME_RECORD_RANGE: u8 = 0x01;
445pub const GIT_REFLOG_RECORD_ENTRY: u8 = 0x01;
446pub const GIT_FETCH_RECORD_REF: u8 = 0x01;
447
448pub const GIT_FOUND_BARE: u8 = 1 << 0;
450pub const GIT_FOUND_LINKED: u8 = 1 << 1;
451pub const GIT_FOUND_SUBMODULE: u8 = 1 << 2;
452
453pub const GIT_FETCH_REF_FORCED: u8 = 1 << 0;
455pub const GIT_FETCH_REF_PRUNED: u8 = 1 << 1;
456pub const GIT_FETCH_REF_NEW: u8 = 1 << 2;
457pub const GIT_FETCH_REF_TAG_UPDATE: u8 = 1 << 3;
462
463pub const GIT_INDEX_INTENT_TO_ADD: u8 = 1 << 0;
465pub const GIT_INDEX_SKIP_WORKTREE: u8 = 1 << 1;
466
467pub type GitOid = [u8; 32];
470
471pub const GIT_OID_NONE: GitOid = [0; 32];
474
475#[derive(Clone, Copy, Debug, PartialEq, Eq)]
478pub struct GitEndpoint {
479 pub kind: u8,
480 pub oid: GitOid,
481}
482
483fn decompress_guarded(data: &[u8]) -> Option<Vec<u8>> {
487 if data.len() < 4 {
488 return None;
489 }
490 let declared = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
491 if declared > crate::MAX_DECOMPRESSED {
492 return None;
493 }
494 lz4_flex::decompress_size_prepended(data).ok()
495}
496
497const MAX_STR: usize = u16::MAX as usize;
503
504fn push_str(buf: &mut Vec<u8>, s: &str) {
505 let b = s.as_bytes();
514 let b = if b.len() > MAX_STR {
515 let mut end = MAX_STR;
518 while end > 0 && !s.is_char_boundary(end) {
519 end -= 1;
520 }
521 &b[..end]
522 } else {
523 b
524 };
525 buf.extend_from_slice(&(b.len() as u16).to_le_bytes());
526 buf.extend_from_slice(b);
527}
528
529fn push_bytes(buf: &mut Vec<u8>, b: &[u8]) {
531 buf.extend_from_slice(&(b.len() as u32).to_le_bytes());
532 buf.extend_from_slice(b);
533}
534
535fn push_oids(buf: &mut Vec<u8>, oids: &[GitOid]) {
536 for oid in oids {
537 buf.extend_from_slice(oid);
538 }
539}
540
541fn take_u8(b: &mut &[u8]) -> Option<u8> {
542 let (&x, rest) = b.split_first()?;
543 *b = rest;
544 Some(x)
545}
546
547fn take_u16(b: &mut &[u8]) -> Option<u16> {
548 if b.len() < 2 {
549 return None;
550 }
551 let v = u16::from_le_bytes([b[0], b[1]]);
552 *b = &b[2..];
553 Some(v)
554}
555
556fn take_i16(b: &mut &[u8]) -> Option<i16> {
557 Some(take_u16(b)? as i16)
558}
559
560fn take_u32(b: &mut &[u8]) -> Option<u32> {
561 if b.len() < 4 {
562 return None;
563 }
564 let v = u32::from_le_bytes(b[0..4].try_into().unwrap());
565 *b = &b[4..];
566 Some(v)
567}
568
569fn take_u64(b: &mut &[u8]) -> Option<u64> {
570 if b.len() < 8 {
571 return None;
572 }
573 let v = u64::from_le_bytes(b[0..8].try_into().unwrap());
574 *b = &b[8..];
575 Some(v)
576}
577
578fn take_i64(b: &mut &[u8]) -> Option<i64> {
579 Some(take_u64(b)? as i64)
580}
581
582fn take_oid(b: &mut &[u8]) -> Option<GitOid> {
583 if b.len() < 32 {
584 return None;
585 }
586 let oid: GitOid = b[0..32].try_into().unwrap();
587 *b = &b[32..];
588 Some(oid)
589}
590
591fn take_oids(b: &mut &[u8], n: usize) -> Option<Vec<GitOid>> {
592 if b.len() < n * 32 {
593 return None;
594 }
595 let mut oids = Vec::with_capacity(n);
596 for _ in 0..n {
597 oids.push(take_oid(b)?);
598 }
599 Some(oids)
600}
601
602fn take_str<'a>(b: &mut &'a [u8]) -> Option<&'a str> {
603 let len = take_u16(b)? as usize;
604 if b.len() < len {
605 return None;
606 }
607 let s = std::str::from_utf8(&b[..len]).ok()?;
608 *b = &b[len..];
609 Some(s)
610}
611
612fn take_bytes<'a>(b: &mut &'a [u8]) -> Option<&'a [u8]> {
613 let len = take_u32(b)? as usize;
614 if b.len() < len {
615 return None;
616 }
617 let bytes = &b[..len];
618 *b = &b[len..];
619 Some(bytes)
620}
621
622fn take_endpoint(b: &mut &[u8]) -> Option<GitEndpoint> {
623 let kind = take_u8(b)?;
624 let oid = take_oid(b)?;
625 Some(GitEndpoint { kind, oid })
626}
627
628fn push_endpoint(buf: &mut Vec<u8>, endpoint: GitEndpoint) {
629 buf.push(endpoint.kind);
630 buf.extend_from_slice(&endpoint.oid);
631}
632
633fn body_of(msg: &[u8], opcode: u8) -> Option<&[u8]> {
635 if msg.first() != Some(&opcode) {
636 return None;
637 }
638 Some(&msg[1..])
639}
640
641#[derive(Clone, Debug, PartialEq, Eq)]
651pub struct GitOpenRequest<'a> {
652 pub nonce: u16,
653 pub flags: u16,
654 pub refs_latency_ms: u16,
656 pub status_latency_ms: u16,
657 pub src_pty_id: u16,
660 pub parent_repo_id: u16,
665 pub ref_prefixes: Vec<&'a str>,
668 pub path: &'a str,
671}
672
673impl GitOpenRequest<'_> {
674 pub fn new(nonce: u16, flags: u16, path: &str) -> GitOpenRequest<'_> {
676 GitOpenRequest {
677 nonce,
678 flags,
679 refs_latency_ms: 0,
680 status_latency_ms: 0,
681 src_pty_id: GIT_OPEN_NO_CONTEXT,
682 parent_repo_id: GIT_OPEN_NO_CONTEXT,
683 ref_prefixes: Vec::new(),
684 path,
685 }
686 }
687}
688
689pub fn msg_git_open(req: &GitOpenRequest<'_>) -> Vec<u8> {
690 let mut msg = Vec::with_capacity(17 + req.path.len());
691 msg.push(C2S_GIT_OPEN);
692 msg.extend_from_slice(&req.nonce.to_le_bytes());
693 msg.extend_from_slice(&req.flags.to_le_bytes());
694 msg.extend_from_slice(&req.refs_latency_ms.to_le_bytes());
695 msg.extend_from_slice(&req.status_latency_ms.to_le_bytes());
696 msg.extend_from_slice(&req.src_pty_id.to_le_bytes());
697 msg.extend_from_slice(&req.parent_repo_id.to_le_bytes());
698 msg.extend_from_slice(&(req.ref_prefixes.len() as u16).to_le_bytes());
699 for prefix in &req.ref_prefixes {
700 push_str(&mut msg, prefix);
701 }
702 push_str(&mut msg, req.path);
703 msg
704}
705
706pub fn parse_git_open(msg: &[u8]) -> Option<GitOpenRequest<'_>> {
707 let mut b = body_of(msg, C2S_GIT_OPEN)?;
708 let nonce = take_u16(&mut b)?;
709 let flags = take_u16(&mut b)?;
710 let refs_latency_ms = take_u16(&mut b)?;
711 let status_latency_ms = take_u16(&mut b)?;
712 let src_pty_id = take_u16(&mut b)?;
713 let parent_repo_id = take_u16(&mut b)?;
714 let n_prefixes = take_u16(&mut b)? as usize;
715 let mut ref_prefixes = Vec::with_capacity(n_prefixes.min(64));
716 for _ in 0..n_prefixes {
717 ref_prefixes.push(take_str(&mut b)?);
718 }
719 let path = take_str(&mut b)?;
720 Some(GitOpenRequest {
721 nonce,
722 flags,
723 refs_latency_ms,
724 status_latency_ms,
725 src_pty_id,
726 parent_repo_id,
727 ref_prefixes,
728 path,
729 })
730}
731
732pub fn git_open_rebase(msg: &[u8], cwd: Option<&str>) -> Option<Vec<u8>> {
737 let req = parse_git_open(msg)?;
738 if req.src_pty_id == GIT_OPEN_NO_CONTEXT {
739 return None;
740 }
741 let joined = cwd.map(|dir| {
742 std::path::Path::new(dir)
743 .join(req.path)
744 .to_string_lossy()
745 .into_owned()
746 });
747 Some(msg_git_open(&GitOpenRequest {
748 nonce: req.nonce,
749 flags: req.flags,
750 refs_latency_ms: req.refs_latency_ms,
751 status_latency_ms: req.status_latency_ms,
752 src_pty_id: GIT_OPEN_NO_CONTEXT,
753 parent_repo_id: req.parent_repo_id,
754 ref_prefixes: req.ref_prefixes,
755 path: joined.as_deref().unwrap_or(req.path),
756 }))
757}
758
759pub fn msg_git_close(repo_id: u16) -> Vec<u8> {
760 let mut msg = Vec::with_capacity(3);
761 msg.push(C2S_GIT_CLOSE);
762 msg.extend_from_slice(&repo_id.to_le_bytes());
763 msg
764}
765
766pub fn parse_git_close(msg: &[u8]) -> Option<u16> {
767 let mut b = body_of(msg, C2S_GIT_CLOSE)?;
768 take_u16(&mut b)
769}
770
771pub fn msg_git_ack(repo_id: u16, state_id: u32) -> Vec<u8> {
772 let mut msg = Vec::with_capacity(7);
773 msg.push(C2S_GIT_ACK);
774 msg.extend_from_slice(&repo_id.to_le_bytes());
775 msg.extend_from_slice(&state_id.to_le_bytes());
776 msg
777}
778
779pub fn parse_git_ack(msg: &[u8]) -> Option<(u16, u32)> {
781 let mut b = body_of(msg, C2S_GIT_ACK)?;
782 let repo_id = take_u16(&mut b)?;
783 let state_id = take_u32(&mut b)?;
784 Some((repo_id, state_id))
785}
786
787#[derive(Clone, Debug, PartialEq, Eq)]
789pub struct GitLogRequest<'a> {
790 pub nonce: u16,
791 pub repo_id: u16,
792 pub flags: u8,
793 pub limit: u16,
795 pub path: &'a str,
797 pub tips: Vec<GitOid>,
799 pub hides: Vec<GitOid>,
800}
801
802pub fn msg_git_log(
803 nonce: u16,
804 repo_id: u16,
805 flags: u8,
806 limit: u16,
807 path: &str,
808 tips: &[GitOid],
809 hides: &[GitOid],
810) -> Vec<u8> {
811 let mut msg = Vec::with_capacity(14 + path.len() + 32 * (tips.len() + hides.len()));
812 msg.push(C2S_GIT_LOG);
813 msg.extend_from_slice(&nonce.to_le_bytes());
814 msg.extend_from_slice(&repo_id.to_le_bytes());
815 msg.push(flags);
816 msg.extend_from_slice(&limit.to_le_bytes());
817 push_str(&mut msg, path);
818 msg.extend_from_slice(&(tips.len() as u16).to_le_bytes());
819 push_oids(&mut msg, tips);
820 msg.extend_from_slice(&(hides.len() as u16).to_le_bytes());
821 push_oids(&mut msg, hides);
822 msg
823}
824
825pub fn parse_git_log(msg: &[u8]) -> Option<GitLogRequest<'_>> {
826 let mut b = body_of(msg, C2S_GIT_LOG)?;
827 let nonce = take_u16(&mut b)?;
828 let repo_id = take_u16(&mut b)?;
829 let flags = take_u8(&mut b)?;
830 let limit = take_u16(&mut b)?;
831 let path = take_str(&mut b)?;
832 let n_tips = take_u16(&mut b)? as usize;
833 let tips = take_oids(&mut b, n_tips)?;
834 let n_hides = take_u16(&mut b)? as usize;
835 let hides = take_oids(&mut b, n_hides)?;
836 Some(GitLogRequest {
837 nonce,
838 repo_id,
839 flags,
840 limit,
841 path,
842 tips,
843 hides,
844 })
845}
846
847#[derive(Clone, Debug, PartialEq, Eq)]
850pub struct GitTreeRequest<'a> {
851 pub nonce: u16,
852 pub repo_id: u16,
853 pub flags: u8,
855 pub oid: GitOid,
856 pub path: &'a str,
857 pub after: &'a str,
858}
859
860pub fn msg_git_tree(req: &GitTreeRequest<'_>) -> Vec<u8> {
861 let mut msg = Vec::with_capacity(42 + req.path.len() + req.after.len());
862 msg.push(C2S_GIT_TREE);
863 msg.extend_from_slice(&req.nonce.to_le_bytes());
864 msg.extend_from_slice(&req.repo_id.to_le_bytes());
865 msg.push(req.flags);
866 msg.extend_from_slice(&req.oid);
867 push_str(&mut msg, req.path);
868 push_str(&mut msg, req.after);
869 msg
870}
871
872pub fn parse_git_tree(msg: &[u8]) -> Option<GitTreeRequest<'_>> {
873 let mut b = body_of(msg, C2S_GIT_TREE)?;
874 let nonce = take_u16(&mut b)?;
875 let repo_id = take_u16(&mut b)?;
876 let flags = take_u8(&mut b)?;
877 let oid = take_oid(&mut b)?;
878 let path = take_str(&mut b)?;
879 let after = take_str(&mut b)?;
880 Some(GitTreeRequest {
881 nonce,
882 repo_id,
883 flags,
884 oid,
885 path,
886 after,
887 })
888}
889
890#[derive(Clone, Debug, PartialEq, Eq)]
892pub struct GitBlobRequest<'a> {
893 pub nonce: u16,
894 pub repo_id: u16,
895 pub flags: u8,
897 pub oid: GitOid,
898 pub path: &'a str,
899 pub offset: u64,
902 pub max_len: u32,
904}
905
906pub fn msg_git_blob(req: &GitBlobRequest<'_>) -> Vec<u8> {
907 let mut msg = Vec::with_capacity(52 + req.path.len());
908 msg.push(C2S_GIT_BLOB);
909 msg.extend_from_slice(&req.nonce.to_le_bytes());
910 msg.extend_from_slice(&req.repo_id.to_le_bytes());
911 msg.push(req.flags);
912 msg.extend_from_slice(&req.oid);
913 push_str(&mut msg, req.path);
914 msg.extend_from_slice(&req.offset.to_le_bytes());
915 msg.extend_from_slice(&req.max_len.to_le_bytes());
916 msg
917}
918
919pub fn parse_git_blob(msg: &[u8]) -> Option<GitBlobRequest<'_>> {
920 let mut b = body_of(msg, C2S_GIT_BLOB)?;
921 let nonce = take_u16(&mut b)?;
922 let repo_id = take_u16(&mut b)?;
923 let flags = take_u8(&mut b)?;
924 let oid = take_oid(&mut b)?;
925 let path = take_str(&mut b)?;
926 let offset = take_u64(&mut b)?;
927 let max_len = take_u32(&mut b)?;
928 Some(GitBlobRequest {
929 nonce,
930 repo_id,
931 flags,
932 oid,
933 path,
934 offset,
935 max_len,
936 })
937}
938
939#[derive(Clone, Debug, PartialEq, Eq)]
941pub struct GitDiffRequest<'a> {
942 pub nonce: u16,
943 pub repo_id: u16,
944 pub flags: u8,
945 pub rename: u8,
948 pub old: GitEndpoint,
949 pub new: GitEndpoint,
950 pub path: &'a str,
952 pub after: &'a str,
954}
955
956pub fn msg_git_diff(req: &GitDiffRequest<'_>) -> Vec<u8> {
957 let mut msg = Vec::with_capacity(77 + req.path.len() + req.after.len());
958 msg.push(C2S_GIT_DIFF);
959 msg.extend_from_slice(&req.nonce.to_le_bytes());
960 msg.extend_from_slice(&req.repo_id.to_le_bytes());
961 msg.push(req.flags);
962 msg.push(req.rename);
963 push_endpoint(&mut msg, req.old);
964 push_endpoint(&mut msg, req.new);
965 push_str(&mut msg, req.path);
966 push_str(&mut msg, req.after);
967 msg
968}
969
970pub fn parse_git_diff(msg: &[u8]) -> Option<GitDiffRequest<'_>> {
971 let mut b = body_of(msg, C2S_GIT_DIFF)?;
972 let nonce = take_u16(&mut b)?;
973 let repo_id = take_u16(&mut b)?;
974 let flags = take_u8(&mut b)?;
975 let rename = take_u8(&mut b)?;
976 let old = take_endpoint(&mut b)?;
977 let new = take_endpoint(&mut b)?;
978 let path = take_str(&mut b)?;
979 let after = take_str(&mut b)?;
980 Some(GitDiffRequest {
981 nonce,
982 repo_id,
983 flags,
984 rename,
985 old,
986 new,
987 path,
988 after,
989 })
990}
991
992#[derive(Clone, Debug, PartialEq, Eq)]
994pub struct GitPatchRequest<'a> {
995 pub nonce: u16,
996 pub repo_id: u16,
997 pub flags: u16,
999 pub context: u8,
1001 pub rename: u8,
1003 pub old: GitEndpoint,
1004 pub new: GitEndpoint,
1005 pub path: &'a str,
1007 pub max_len: u32,
1008 pub after: &'a str,
1010 pub after_pos: u64,
1013}
1014
1015pub fn msg_git_patch(req: &GitPatchRequest<'_>) -> Vec<u8> {
1016 let mut msg = Vec::with_capacity(90 + req.path.len() + req.after.len());
1017 msg.push(C2S_GIT_PATCH);
1018 msg.extend_from_slice(&req.nonce.to_le_bytes());
1019 msg.extend_from_slice(&req.repo_id.to_le_bytes());
1020 msg.extend_from_slice(&req.flags.to_le_bytes());
1021 msg.push(req.context);
1022 msg.push(req.rename);
1023 push_endpoint(&mut msg, req.old);
1024 push_endpoint(&mut msg, req.new);
1025 push_str(&mut msg, req.path);
1026 msg.extend_from_slice(&req.max_len.to_le_bytes());
1027 push_str(&mut msg, req.after);
1028 msg.extend_from_slice(&req.after_pos.to_le_bytes());
1029 msg
1030}
1031
1032pub fn parse_git_patch(msg: &[u8]) -> Option<GitPatchRequest<'_>> {
1033 let mut b = body_of(msg, C2S_GIT_PATCH)?;
1034 let nonce = take_u16(&mut b)?;
1035 let repo_id = take_u16(&mut b)?;
1036 let flags = take_u16(&mut b)?;
1037 let context = take_u8(&mut b)?;
1038 let rename = take_u8(&mut b)?;
1039 let old = take_endpoint(&mut b)?;
1040 let new = take_endpoint(&mut b)?;
1041 let path = take_str(&mut b)?;
1042 let max_len = take_u32(&mut b)?;
1043 let after = take_str(&mut b)?;
1044 let after_pos = take_u64(&mut b)?;
1045 Some(GitPatchRequest {
1046 nonce,
1047 repo_id,
1048 flags,
1049 context,
1050 rename,
1051 old,
1052 new,
1053 path,
1054 max_len,
1055 after,
1056 after_pos,
1057 })
1058}
1059
1060#[derive(Clone, Debug, PartialEq, Eq)]
1062pub struct GitIndexRequest<'a> {
1063 pub nonce: u16,
1064 pub repo_id: u16,
1065 pub flags: u8,
1067 pub path: &'a str,
1068 pub after: &'a str,
1069}
1070
1071pub fn msg_git_index(req: &GitIndexRequest<'_>) -> Vec<u8> {
1072 let mut msg = Vec::with_capacity(10 + req.path.len() + req.after.len());
1073 msg.push(C2S_GIT_INDEX);
1074 msg.extend_from_slice(&req.nonce.to_le_bytes());
1075 msg.extend_from_slice(&req.repo_id.to_le_bytes());
1076 msg.push(req.flags);
1077 push_str(&mut msg, req.path);
1078 push_str(&mut msg, req.after);
1079 msg
1080}
1081
1082pub fn parse_git_index(msg: &[u8]) -> Option<GitIndexRequest<'_>> {
1083 let mut b = body_of(msg, C2S_GIT_INDEX)?;
1084 let nonce = take_u16(&mut b)?;
1085 let repo_id = take_u16(&mut b)?;
1086 let flags = take_u8(&mut b)?;
1087 let path = take_str(&mut b)?;
1088 let after = take_str(&mut b)?;
1089 Some(GitIndexRequest {
1090 nonce,
1091 repo_id,
1092 flags,
1093 path,
1094 after,
1095 })
1096}
1097
1098pub fn msg_git_cancel(nonce: u16) -> Vec<u8> {
1099 let mut msg = Vec::with_capacity(3);
1100 msg.push(C2S_GIT_CANCEL);
1101 msg.extend_from_slice(&nonce.to_le_bytes());
1102 msg
1103}
1104
1105pub fn parse_git_cancel(msg: &[u8]) -> Option<u16> {
1106 let mut b = body_of(msg, C2S_GIT_CANCEL)?;
1107 take_u16(&mut b)
1108}
1109
1110pub fn msg_git_base(nonce: u16, repo_id: u16, oids: &[GitOid]) -> Vec<u8> {
1111 let mut msg = Vec::with_capacity(6 + 32 * oids.len());
1112 msg.push(C2S_GIT_BASE);
1113 msg.extend_from_slice(&nonce.to_le_bytes());
1114 msg.extend_from_slice(&repo_id.to_le_bytes());
1115 msg.push(oids.len() as u8);
1116 push_oids(&mut msg, oids);
1117 msg
1118}
1119
1120pub fn parse_git_base(msg: &[u8]) -> Option<(u16, u16, Vec<GitOid>)> {
1122 let mut b = body_of(msg, C2S_GIT_BASE)?;
1123 let nonce = take_u16(&mut b)?;
1124 let repo_id = take_u16(&mut b)?;
1125 let n = take_u8(&mut b)? as usize;
1126 let oids = take_oids(&mut b, n)?;
1127 Some((nonce, repo_id, oids))
1128}
1129
1130pub fn msg_git_resolve(nonce: u16, repo_id: u16, spec: &str) -> Vec<u8> {
1131 let mut msg = Vec::with_capacity(7 + spec.len());
1132 msg.push(C2S_GIT_RESOLVE);
1133 msg.extend_from_slice(&nonce.to_le_bytes());
1134 msg.extend_from_slice(&repo_id.to_le_bytes());
1135 push_str(&mut msg, spec);
1136 msg
1137}
1138
1139pub fn parse_git_resolve(msg: &[u8]) -> Option<(u16, u16, &str)> {
1141 let mut b = body_of(msg, C2S_GIT_RESOLVE)?;
1142 let nonce = take_u16(&mut b)?;
1143 let repo_id = take_u16(&mut b)?;
1144 let spec = take_str(&mut b)?;
1145 Some((nonce, repo_id, spec))
1146}
1147
1148pub fn msg_git_log_watch(log_id: u16, repo_id: u16, flags: u8, limit: u16, spec: &str) -> Vec<u8> {
1149 let mut msg = Vec::with_capacity(10 + spec.len());
1150 msg.push(C2S_GIT_LOG_WATCH);
1151 msg.extend_from_slice(&log_id.to_le_bytes());
1152 msg.extend_from_slice(&repo_id.to_le_bytes());
1153 msg.push(flags);
1154 msg.extend_from_slice(&limit.to_le_bytes());
1155 push_str(&mut msg, spec);
1156 msg
1157}
1158
1159pub fn parse_git_log_watch(msg: &[u8]) -> Option<(u16, u16, u8, u16, &str)> {
1161 let mut b = body_of(msg, C2S_GIT_LOG_WATCH)?;
1162 let log_id = take_u16(&mut b)?;
1163 let repo_id = take_u16(&mut b)?;
1164 let flags = take_u8(&mut b)?;
1165 let limit = take_u16(&mut b)?;
1166 let spec = take_str(&mut b)?;
1167 Some((log_id, repo_id, flags, limit, spec))
1168}
1169
1170pub fn msg_git_log_unwatch(log_id: u16, repo_id: u16) -> Vec<u8> {
1171 let mut msg = Vec::with_capacity(5);
1172 msg.push(C2S_GIT_LOG_UNWATCH);
1173 msg.extend_from_slice(&log_id.to_le_bytes());
1174 msg.extend_from_slice(&repo_id.to_le_bytes());
1175 msg
1176}
1177
1178pub fn parse_git_log_unwatch(msg: &[u8]) -> Option<(u16, u16)> {
1180 let mut b = body_of(msg, C2S_GIT_LOG_UNWATCH)?;
1181 let log_id = take_u16(&mut b)?;
1182 let repo_id = take_u16(&mut b)?;
1183 Some((log_id, repo_id))
1184}
1185
1186pub fn msg_git_log_ack(log_id: u16, repo_id: u16, update_id: u32) -> Vec<u8> {
1187 let mut msg = Vec::with_capacity(9);
1188 msg.push(C2S_GIT_LOG_ACK);
1189 msg.extend_from_slice(&log_id.to_le_bytes());
1190 msg.extend_from_slice(&repo_id.to_le_bytes());
1191 msg.extend_from_slice(&update_id.to_le_bytes());
1192 msg
1193}
1194
1195pub fn parse_git_log_ack(msg: &[u8]) -> Option<(u16, u16, u32)> {
1197 let mut b = body_of(msg, C2S_GIT_LOG_ACK)?;
1198 let log_id = take_u16(&mut b)?;
1199 let repo_id = take_u16(&mut b)?;
1200 let update_id = take_u32(&mut b)?;
1201 Some((log_id, repo_id, update_id))
1202}
1203
1204#[derive(Clone, Debug, PartialEq, Eq)]
1206pub struct GitDiscoverRequest<'a> {
1207 pub nonce: u16,
1208 pub flags: u8,
1209 pub depth: u8,
1211 pub path: &'a str,
1213 pub after: &'a str,
1214}
1215
1216pub fn msg_git_discover(req: &GitDiscoverRequest<'_>) -> Vec<u8> {
1217 let mut msg = Vec::with_capacity(9 + req.path.len() + req.after.len());
1218 msg.push(C2S_GIT_DISCOVER);
1219 msg.extend_from_slice(&req.nonce.to_le_bytes());
1220 msg.push(req.flags);
1221 msg.push(req.depth);
1222 push_str(&mut msg, req.path);
1223 push_str(&mut msg, req.after);
1224 msg
1225}
1226
1227pub fn parse_git_discover(msg: &[u8]) -> Option<GitDiscoverRequest<'_>> {
1228 let mut b = body_of(msg, C2S_GIT_DISCOVER)?;
1229 let nonce = take_u16(&mut b)?;
1230 let flags = take_u8(&mut b)?;
1231 let depth = take_u8(&mut b)?;
1232 let path = take_str(&mut b)?;
1233 let after = take_str(&mut b)?;
1234 Some(GitDiscoverRequest {
1235 nonce,
1236 flags,
1237 depth,
1238 path,
1239 after,
1240 })
1241}
1242
1243#[derive(Clone, Debug, PartialEq, Eq)]
1245pub struct GitBlameRequest<'a> {
1246 pub nonce: u16,
1247 pub repo_id: u16,
1248 pub flags: u8,
1249 pub oid: GitOid,
1252 pub start_line: u32,
1255 pub line_count: u32,
1257 pub path: &'a str,
1258}
1259
1260pub fn msg_git_blame(req: &GitBlameRequest<'_>) -> Vec<u8> {
1261 let mut msg = Vec::with_capacity(48 + req.path.len());
1262 msg.push(C2S_GIT_BLAME);
1263 msg.extend_from_slice(&req.nonce.to_le_bytes());
1264 msg.extend_from_slice(&req.repo_id.to_le_bytes());
1265 msg.push(req.flags);
1266 msg.extend_from_slice(&req.oid);
1267 msg.extend_from_slice(&req.start_line.to_le_bytes());
1268 msg.extend_from_slice(&req.line_count.to_le_bytes());
1269 push_str(&mut msg, req.path);
1270 msg
1271}
1272
1273pub fn parse_git_blame(msg: &[u8]) -> Option<GitBlameRequest<'_>> {
1274 let mut b = body_of(msg, C2S_GIT_BLAME)?;
1275 let nonce = take_u16(&mut b)?;
1276 let repo_id = take_u16(&mut b)?;
1277 let flags = take_u8(&mut b)?;
1278 let oid = take_oid(&mut b)?;
1279 let start_line = take_u32(&mut b)?;
1280 let line_count = take_u32(&mut b)?;
1281 let path = take_str(&mut b)?;
1282 Some(GitBlameRequest {
1283 nonce,
1284 repo_id,
1285 flags,
1286 oid,
1287 start_line,
1288 line_count,
1289 path,
1290 })
1291}
1292
1293#[derive(Clone, Debug, PartialEq, Eq)]
1295pub struct GitReflogRequest<'a> {
1296 pub nonce: u16,
1297 pub repo_id: u16,
1298 pub flags: u8,
1299 pub limit: u16,
1301 pub ref_name: &'a str,
1303 pub after_pos: u64,
1310}
1311
1312pub fn msg_git_reflog(req: &GitReflogRequest<'_>) -> Vec<u8> {
1313 let mut msg = Vec::with_capacity(18 + req.ref_name.len());
1314 msg.push(C2S_GIT_REFLOG);
1315 msg.extend_from_slice(&req.nonce.to_le_bytes());
1316 msg.extend_from_slice(&req.repo_id.to_le_bytes());
1317 msg.push(req.flags);
1318 msg.extend_from_slice(&req.limit.to_le_bytes());
1319 msg.extend_from_slice(&req.after_pos.to_le_bytes());
1320 push_str(&mut msg, req.ref_name);
1321 msg
1322}
1323
1324pub fn parse_git_reflog(msg: &[u8]) -> Option<GitReflogRequest<'_>> {
1325 let mut b = body_of(msg, C2S_GIT_REFLOG)?;
1326 let nonce = take_u16(&mut b)?;
1327 let repo_id = take_u16(&mut b)?;
1328 let flags = take_u8(&mut b)?;
1329 let limit = take_u16(&mut b)?;
1330 let after_pos = take_u64(&mut b)?;
1331 let ref_name = take_str(&mut b)?;
1332 Some(GitReflogRequest {
1333 nonce,
1334 repo_id,
1335 flags,
1336 limit,
1337 ref_name,
1338 after_pos,
1339 })
1340}
1341
1342#[derive(Clone, Debug, PartialEq, Eq)]
1344pub struct GitFetchRequest<'a> {
1345 pub nonce: u16,
1346 pub repo_id: u16,
1347 pub flags: u8,
1348 pub timeout_ms: u32,
1350 pub remote: &'a str,
1352 pub refspecs: Vec<&'a str>,
1354}
1355
1356pub fn msg_git_fetch(req: &GitFetchRequest<'_>) -> Vec<u8> {
1357 let mut msg = Vec::with_capacity(14 + req.remote.len());
1358 msg.push(C2S_GIT_FETCH);
1359 msg.extend_from_slice(&req.nonce.to_le_bytes());
1360 msg.extend_from_slice(&req.repo_id.to_le_bytes());
1361 msg.push(req.flags);
1362 msg.extend_from_slice(&req.timeout_ms.to_le_bytes());
1363 push_str(&mut msg, req.remote);
1364 msg.extend_from_slice(&(req.refspecs.len() as u16).to_le_bytes());
1365 for spec in &req.refspecs {
1366 push_str(&mut msg, spec);
1367 }
1368 msg
1369}
1370
1371pub fn parse_git_fetch(msg: &[u8]) -> Option<GitFetchRequest<'_>> {
1372 let mut b = body_of(msg, C2S_GIT_FETCH)?;
1373 let nonce = take_u16(&mut b)?;
1374 let repo_id = take_u16(&mut b)?;
1375 let flags = take_u8(&mut b)?;
1376 let timeout_ms = take_u32(&mut b)?;
1377 let remote = take_str(&mut b)?;
1378 let n = take_u16(&mut b)? as usize;
1379 let mut refspecs = Vec::with_capacity(n.min(64));
1380 for _ in 0..n {
1381 refspecs.push(take_str(&mut b)?);
1382 }
1383 Some(GitFetchRequest {
1384 nonce,
1385 repo_id,
1386 flags,
1387 timeout_ms,
1388 remote,
1389 refspecs,
1390 })
1391}
1392
1393#[derive(Clone, Debug, PartialEq, Eq)]
1399pub struct GitRepoInfo<'a> {
1400 pub nonce: u16,
1401 pub repo_id: u16,
1402 pub status: u8,
1403 pub oid_format: u8,
1404 pub flags: u8,
1405 pub workdir: &'a str,
1407 pub gitdir: &'a str,
1408}
1409
1410pub fn msg_git_repo(
1411 nonce: u16,
1412 repo_id: u16,
1413 status: u8,
1414 oid_format: u8,
1415 flags: u8,
1416 workdir: &str,
1417 gitdir: &str,
1418) -> Vec<u8> {
1419 let mut msg = Vec::with_capacity(12 + workdir.len() + gitdir.len());
1420 msg.push(S2C_GIT_REPO);
1421 msg.extend_from_slice(&nonce.to_le_bytes());
1422 msg.extend_from_slice(&repo_id.to_le_bytes());
1423 msg.push(status);
1424 msg.push(oid_format);
1425 msg.push(flags);
1426 push_str(&mut msg, workdir);
1427 push_str(&mut msg, gitdir);
1428 msg
1429}
1430
1431pub fn parse_git_repo(msg: &[u8]) -> Option<GitRepoInfo<'_>> {
1432 let mut b = body_of(msg, S2C_GIT_REPO)?;
1433 let nonce = take_u16(&mut b)?;
1434 let repo_id = take_u16(&mut b)?;
1435 let status = take_u8(&mut b)?;
1436 let oid_format = take_u8(&mut b)?;
1437 let flags = take_u8(&mut b)?;
1438 let workdir = take_str(&mut b)?;
1439 let gitdir = take_str(&mut b)?;
1440 Some(GitRepoInfo {
1441 nonce,
1442 repo_id,
1443 status,
1444 oid_format,
1445 flags,
1446 workdir,
1447 gitdir,
1448 })
1449}
1450
1451pub fn msg_git_state(repo_id: u16, state_id: u32, flags: u8, records: &[u8]) -> Vec<u8> {
1453 let compressed = lz4_flex::compress_prepend_size(records);
1454 let mut msg = Vec::with_capacity(8 + compressed.len());
1455 msg.push(S2C_GIT_STATE);
1456 msg.extend_from_slice(&repo_id.to_le_bytes());
1457 msg.extend_from_slice(&state_id.to_le_bytes());
1458 msg.push(flags);
1459 msg.extend_from_slice(&compressed);
1460 msg
1461}
1462
1463pub fn parse_git_state(msg: &[u8]) -> Option<(u16, u32, u8, Vec<u8>)> {
1466 let mut b = body_of(msg, S2C_GIT_STATE)?;
1467 let repo_id = take_u16(&mut b)?;
1468 let state_id = take_u32(&mut b)?;
1469 let flags = take_u8(&mut b)?;
1470 let records = decompress_guarded(b)?;
1471 Some((repo_id, state_id, flags, records))
1472}
1473
1474pub fn msg_git_closed(repo_id: u16, reason: u8) -> Vec<u8> {
1475 let mut msg = Vec::with_capacity(4);
1476 msg.push(S2C_GIT_CLOSED);
1477 msg.extend_from_slice(&repo_id.to_le_bytes());
1478 msg.push(reason);
1479 msg
1480}
1481
1482pub fn parse_git_closed(msg: &[u8]) -> Option<(u16, u8)> {
1484 let mut b = body_of(msg, S2C_GIT_CLOSED)?;
1485 let repo_id = take_u16(&mut b)?;
1486 let reason = take_u8(&mut b)?;
1487 Some((repo_id, reason))
1488}
1489
1490pub fn msg_git_commits(
1492 nonce: u16,
1493 status: u8,
1494 flags: u8,
1495 frontier: &[GitOid],
1496 records: &[u8],
1497) -> Vec<u8> {
1498 let compressed = lz4_flex::compress_prepend_size(records);
1499 let mut msg = Vec::with_capacity(7 + 32 * frontier.len() + compressed.len());
1500 msg.push(S2C_GIT_COMMITS);
1501 msg.extend_from_slice(&nonce.to_le_bytes());
1502 msg.push(status);
1503 msg.push(flags);
1504 msg.extend_from_slice(&(frontier.len() as u16).to_le_bytes());
1505 push_oids(&mut msg, frontier);
1506 msg.extend_from_slice(&compressed);
1507 msg
1508}
1509
1510#[derive(Clone, Debug, PartialEq, Eq)]
1512pub struct GitCommitsPage {
1513 pub nonce: u16,
1514 pub status: u8,
1515 pub flags: u8,
1516 pub frontier: Vec<GitOid>,
1519 pub records: Vec<u8>,
1521}
1522
1523pub fn parse_git_commits(msg: &[u8]) -> Option<GitCommitsPage> {
1524 let mut b = body_of(msg, S2C_GIT_COMMITS)?;
1525 let nonce = take_u16(&mut b)?;
1526 let status = take_u8(&mut b)?;
1527 let flags = take_u8(&mut b)?;
1528 let n = take_u16(&mut b)? as usize;
1529 let frontier = take_oids(&mut b, n)?;
1530 let records = decompress_guarded(b)?;
1531 Some(GitCommitsPage {
1532 nonce,
1533 status,
1534 flags,
1535 frontier,
1536 records,
1537 })
1538}
1539
1540fn msg_nonce_status_flags_lz4(
1543 opcode: u8,
1544 nonce: u16,
1545 status: u8,
1546 flags: u8,
1547 payload: &[u8],
1548) -> Vec<u8> {
1549 let compressed = lz4_flex::compress_prepend_size(payload);
1550 let mut msg = Vec::with_capacity(5 + compressed.len());
1551 msg.push(opcode);
1552 msg.extend_from_slice(&nonce.to_le_bytes());
1553 msg.push(status);
1554 msg.push(flags);
1555 msg.extend_from_slice(&compressed);
1556 msg
1557}
1558
1559fn parse_nonce_status_flags_lz4(msg: &[u8], opcode: u8) -> Option<(u16, u8, u8, Vec<u8>)> {
1561 let mut b = body_of(msg, opcode)?;
1562 let nonce = take_u16(&mut b)?;
1563 let status = take_u8(&mut b)?;
1564 let flags = take_u8(&mut b)?;
1565 let payload = decompress_guarded(b)?;
1566 Some((nonce, status, flags, payload))
1567}
1568
1569pub fn msg_git_tree_resp(nonce: u16, status: u8, flags: u8, records: &[u8]) -> Vec<u8> {
1571 msg_nonce_status_flags_lz4(S2C_GIT_TREE, nonce, status, flags, records)
1572}
1573
1574pub fn parse_git_tree_resp(msg: &[u8]) -> Option<(u16, u8, u8, Vec<u8>)> {
1576 parse_nonce_status_flags_lz4(msg, S2C_GIT_TREE)
1577}
1578
1579pub fn msg_git_blob_resp(nonce: u16, status: u8, size: u64, data: &[u8]) -> Vec<u8> {
1582 let compressed = lz4_flex::compress_prepend_size(data);
1583 let mut msg = Vec::with_capacity(12 + compressed.len());
1584 msg.push(S2C_GIT_BLOB);
1585 msg.extend_from_slice(&nonce.to_le_bytes());
1586 msg.push(status);
1587 msg.extend_from_slice(&size.to_le_bytes());
1588 msg.extend_from_slice(&compressed);
1589 msg
1590}
1591
1592pub fn parse_git_blob_resp(msg: &[u8]) -> Option<(u16, u8, u64, Vec<u8>)> {
1594 let mut b = body_of(msg, S2C_GIT_BLOB)?;
1595 let nonce = take_u16(&mut b)?;
1596 let status = take_u8(&mut b)?;
1597 let size = take_u64(&mut b)?;
1598 let data = decompress_guarded(b)?;
1599 Some((nonce, status, size, data))
1600}
1601
1602pub fn msg_git_diff_resp(nonce: u16, status: u8, flags: u8, records: &[u8]) -> Vec<u8> {
1604 msg_nonce_status_flags_lz4(S2C_GIT_DIFF, nonce, status, flags, records)
1605}
1606
1607pub fn parse_git_diff_resp(msg: &[u8]) -> Option<(u16, u8, u8, Vec<u8>)> {
1609 parse_nonce_status_flags_lz4(msg, S2C_GIT_DIFF)
1610}
1611
1612pub fn msg_git_patch_resp(nonce: u16, status: u8, flags: u8, data: &[u8]) -> Vec<u8> {
1615 msg_nonce_status_flags_lz4(S2C_GIT_PATCH, nonce, status, flags, data)
1616}
1617
1618pub fn parse_git_patch_resp(msg: &[u8]) -> Option<(u16, u8, u8, Vec<u8>)> {
1620 parse_nonce_status_flags_lz4(msg, S2C_GIT_PATCH)
1621}
1622
1623pub fn msg_git_index_resp(nonce: u16, status: u8, flags: u8, records: &[u8]) -> Vec<u8> {
1625 msg_nonce_status_flags_lz4(S2C_GIT_INDEX, nonce, status, flags, records)
1626}
1627
1628pub fn parse_git_index_resp(msg: &[u8]) -> Option<(u16, u8, u8, Vec<u8>)> {
1630 parse_nonce_status_flags_lz4(msg, S2C_GIT_INDEX)
1631}
1632
1633pub fn msg_git_base_resp(nonce: u16, status: u8, bases: &[GitOid]) -> Vec<u8> {
1636 let mut msg = Vec::with_capacity(5 + 32 * bases.len());
1637 msg.push(S2C_GIT_BASE);
1638 msg.extend_from_slice(&nonce.to_le_bytes());
1639 msg.push(status);
1640 msg.push(bases.len() as u8);
1641 push_oids(&mut msg, bases);
1642 msg
1643}
1644
1645pub fn parse_git_base_resp(msg: &[u8]) -> Option<(u16, u8, Vec<GitOid>)> {
1647 let mut b = body_of(msg, S2C_GIT_BASE)?;
1648 let nonce = take_u16(&mut b)?;
1649 let status = take_u8(&mut b)?;
1650 let n = take_u8(&mut b)? as usize;
1651 let bases = take_oids(&mut b, n)?;
1652 Some((nonce, status, bases))
1653}
1654
1655pub fn msg_git_resolve_resp(nonce: u16, status: u8, tips: &[GitOid], hides: &[GitOid]) -> Vec<u8> {
1656 let mut msg = Vec::with_capacity(7 + 32 * (tips.len() + hides.len()));
1657 msg.push(S2C_GIT_RESOLVE);
1658 msg.extend_from_slice(&nonce.to_le_bytes());
1659 msg.push(status);
1660 msg.extend_from_slice(&(tips.len() as u16).to_le_bytes());
1661 push_oids(&mut msg, tips);
1662 msg.extend_from_slice(&(hides.len() as u16).to_le_bytes());
1663 push_oids(&mut msg, hides);
1664 msg
1665}
1666
1667pub fn parse_git_resolve_resp(msg: &[u8]) -> Option<(u16, u8, Vec<GitOid>, Vec<GitOid>)> {
1669 let mut b = body_of(msg, S2C_GIT_RESOLVE)?;
1670 let nonce = take_u16(&mut b)?;
1671 let status = take_u8(&mut b)?;
1672 let n_tips = take_u16(&mut b)? as usize;
1673 let tips = take_oids(&mut b, n_tips)?;
1674 let n_hides = take_u16(&mut b)? as usize;
1675 let hides = take_oids(&mut b, n_hides)?;
1676 Some((nonce, status, tips, hides))
1677}
1678
1679pub fn msg_git_log_page(
1680 log_id: u16,
1681 update_id: u32,
1682 status: u8,
1683 flags: u8,
1684 frontier: &[GitOid],
1685 records: &[u8],
1686) -> Vec<u8> {
1687 let compressed = lz4_flex::compress_prepend_size(records);
1688 let mut msg = Vec::with_capacity(11 + 32 * frontier.len() + compressed.len());
1689 msg.push(S2C_GIT_LOG_PAGE);
1690 msg.extend_from_slice(&log_id.to_le_bytes());
1691 msg.extend_from_slice(&update_id.to_le_bytes());
1692 msg.push(status);
1693 msg.push(flags);
1694 msg.extend_from_slice(&(frontier.len() as u16).to_le_bytes());
1695 push_oids(&mut msg, frontier);
1696 msg.extend_from_slice(&compressed);
1697 msg
1698}
1699
1700#[derive(Clone, Debug, PartialEq, Eq)]
1702pub struct GitLogPage {
1703 pub log_id: u16,
1704 pub update_id: u32,
1706 pub status: u8,
1707 pub flags: u8,
1708 pub frontier: Vec<GitOid>,
1709 pub records: Vec<u8>,
1710}
1711
1712pub fn parse_git_log_page(msg: &[u8]) -> Option<GitLogPage> {
1713 let mut b = body_of(msg, S2C_GIT_LOG_PAGE)?;
1714 let log_id = take_u16(&mut b)?;
1715 let update_id = take_u32(&mut b)?;
1716 let status = take_u8(&mut b)?;
1717 let flags = take_u8(&mut b)?;
1718 let n = take_u16(&mut b)? as usize;
1719 let frontier = take_oids(&mut b, n)?;
1720 let records = decompress_guarded(b)?;
1721 Some(GitLogPage {
1722 log_id,
1723 update_id,
1724 status,
1725 flags,
1726 frontier,
1727 records,
1728 })
1729}
1730
1731fn begin_record(buf: &mut Vec<u8>) -> usize {
1742 let start = buf.len();
1743 buf.extend_from_slice(&0u32.to_le_bytes());
1744 start
1745}
1746
1747fn end_record(buf: &mut [u8], start: usize) {
1748 let len = (buf.len() - start - 4) as u32;
1749 buf[start..start + 4].copy_from_slice(&len.to_le_bytes());
1750}
1751
1752fn push_cursor_record(buf: &mut Vec<u8>, after: &str, pos: u64) {
1755 let start = begin_record(buf);
1756 buf.push(GIT_RECORD_CURSOR);
1757 push_str(buf, after);
1758 buf.extend_from_slice(&pos.to_le_bytes());
1759 end_record(buf, start);
1760}
1761
1762fn take_cursor<'a>(b: &mut &'a [u8]) -> Option<(&'a str, u64)> {
1763 let after = take_str(b)?;
1764 let pos = take_u64(b)?;
1765 Some((after, pos))
1766}
1767
1768fn next_record<'a>(data: &mut &'a [u8]) -> Option<(u8, &'a [u8])> {
1771 if data.len() < 4 {
1772 return None;
1773 }
1774 let rec_len = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
1775 if rec_len == 0 || data.len() < 4 + rec_len {
1776 return None;
1777 }
1778 let body = &data[4..4 + rec_len];
1779 *data = &data[4 + rec_len..];
1780 Some((body[0], &body[1..]))
1781}
1782
1783#[derive(Clone, Debug, PartialEq, Eq)]
1785pub enum GitStateRecord<'a> {
1786 Head {
1789 flags: u8,
1790 oid: GitOid,
1791 name: &'a str,
1792 },
1793 Ref {
1798 flags: u8,
1799 oid: GitOid,
1800 peeled: GitOid,
1801 name: &'a str,
1802 target: &'a str,
1803 },
1804 Op {
1807 op: u8,
1808 oid: GitOid,
1809 detail: &'a str,
1810 },
1811 Status {
1823 staged: u8,
1824 unstaged: u8,
1825 flags: u8,
1826 oid: GitOid,
1827 old_path: &'a str,
1828 path: &'a str,
1829 },
1830 Upstream {
1834 flags: u8,
1835 ahead: u32,
1836 behind: u32,
1837 name: &'a str,
1838 upstream: &'a str,
1839 },
1840 Stash {
1843 index: u16,
1844 oid: GitOid,
1845 time: i64,
1846 tz: i16,
1847 msg: &'a str,
1848 },
1849 Remote {
1857 flags: u8,
1858 name: &'a str,
1859 fetch_url: &'a str,
1860 push_url: &'a str,
1861 },
1862}
1863
1864pub fn append_git_state_record(buf: &mut Vec<u8>, record: &GitStateRecord<'_>) {
1866 let start = begin_record(buf);
1867 match record {
1868 GitStateRecord::Head { flags, oid, name } => {
1869 buf.push(GIT_STATE_RECORD_HEAD);
1870 buf.push(*flags);
1871 buf.extend_from_slice(oid);
1872 push_str(buf, name);
1873 }
1874 GitStateRecord::Ref {
1875 flags,
1876 oid,
1877 peeled,
1878 name,
1879 target,
1880 } => {
1881 buf.push(GIT_STATE_RECORD_REF);
1882 buf.push(*flags);
1883 buf.extend_from_slice(oid);
1884 buf.extend_from_slice(peeled);
1885 push_str(buf, name);
1886 push_str(buf, target);
1887 }
1888 GitStateRecord::Op { op, oid, detail } => {
1889 buf.push(GIT_STATE_RECORD_OP);
1890 buf.push(*op);
1891 buf.extend_from_slice(oid);
1892 push_str(buf, detail);
1893 }
1894 GitStateRecord::Status {
1895 staged,
1896 unstaged,
1897 flags,
1898 oid,
1899 old_path,
1900 path,
1901 } => {
1902 buf.push(GIT_STATE_RECORD_STATUS);
1903 buf.push(*staged);
1904 buf.push(*unstaged);
1905 buf.push(*flags);
1906 buf.extend_from_slice(oid);
1907 push_str(buf, old_path);
1908 push_str(buf, path);
1909 }
1910 GitStateRecord::Upstream {
1911 flags,
1912 ahead,
1913 behind,
1914 name,
1915 upstream,
1916 } => {
1917 buf.push(GIT_STATE_RECORD_UPSTREAM);
1918 buf.push(*flags);
1919 buf.extend_from_slice(&ahead.to_le_bytes());
1920 buf.extend_from_slice(&behind.to_le_bytes());
1921 push_str(buf, name);
1922 push_str(buf, upstream);
1923 }
1924 GitStateRecord::Stash {
1925 index,
1926 oid,
1927 time,
1928 tz,
1929 msg,
1930 } => {
1931 buf.push(GIT_STATE_RECORD_STASH);
1932 buf.extend_from_slice(&index.to_le_bytes());
1933 buf.extend_from_slice(oid);
1934 buf.extend_from_slice(&time.to_le_bytes());
1935 buf.extend_from_slice(&tz.to_le_bytes());
1936 push_str(buf, msg);
1937 }
1938 GitStateRecord::Remote {
1939 flags,
1940 name,
1941 fetch_url,
1942 push_url,
1943 } => {
1944 buf.push(GIT_STATE_RECORD_REMOTE);
1945 buf.push(*flags);
1946 push_str(buf, name);
1947 push_str(buf, fetch_url);
1948 push_str(buf, push_url);
1949 }
1950 }
1951 end_record(buf, start);
1952}
1953
1954pub struct GitStateRecordIter<'a> {
1955 data: &'a [u8],
1956}
1957
1958pub fn git_state_records(data: &[u8]) -> GitStateRecordIter<'_> {
1960 GitStateRecordIter { data }
1961}
1962
1963impl<'a> Iterator for GitStateRecordIter<'a> {
1964 type Item = GitStateRecord<'a>;
1965
1966 fn next(&mut self) -> Option<GitStateRecord<'a>> {
1967 loop {
1968 let (kind, mut b) = next_record(&mut self.data)?;
1969 match kind {
1970 GIT_STATE_RECORD_HEAD => {
1971 let flags = take_u8(&mut b)?;
1972 let oid = take_oid(&mut b)?;
1973 let name = take_str(&mut b)?;
1974 return Some(GitStateRecord::Head { flags, oid, name });
1975 }
1976 GIT_STATE_RECORD_REF => {
1977 let flags = take_u8(&mut b)?;
1978 let oid = take_oid(&mut b)?;
1979 let peeled = take_oid(&mut b)?;
1980 let name = take_str(&mut b)?;
1981 let target = take_str(&mut b)?;
1982 return Some(GitStateRecord::Ref {
1983 flags,
1984 oid,
1985 peeled,
1986 name,
1987 target,
1988 });
1989 }
1990 GIT_STATE_RECORD_OP => {
1991 let op = take_u8(&mut b)?;
1992 let oid = take_oid(&mut b)?;
1993 let detail = take_str(&mut b)?;
1994 return Some(GitStateRecord::Op { op, oid, detail });
1995 }
1996 GIT_STATE_RECORD_STATUS => {
1997 let staged = take_u8(&mut b)?;
1998 let unstaged = take_u8(&mut b)?;
1999 let flags = take_u8(&mut b)?;
2000 let oid = take_oid(&mut b)?;
2001 let old_path = take_str(&mut b)?;
2002 let path = take_str(&mut b)?;
2003 return Some(GitStateRecord::Status {
2004 staged,
2005 unstaged,
2006 flags,
2007 oid,
2008 old_path,
2009 path,
2010 });
2011 }
2012 GIT_STATE_RECORD_UPSTREAM => {
2013 let flags = take_u8(&mut b)?;
2014 let ahead = take_u32(&mut b)?;
2015 let behind = take_u32(&mut b)?;
2016 let name = take_str(&mut b)?;
2017 let upstream = take_str(&mut b)?;
2018 return Some(GitStateRecord::Upstream {
2019 flags,
2020 ahead,
2021 behind,
2022 name,
2023 upstream,
2024 });
2025 }
2026 GIT_STATE_RECORD_STASH => {
2027 let index = take_u16(&mut b)?;
2028 let oid = take_oid(&mut b)?;
2029 let time = take_i64(&mut b)?;
2030 let tz = take_i16(&mut b)?;
2031 let msg = take_str(&mut b)?;
2032 return Some(GitStateRecord::Stash {
2033 index,
2034 oid,
2035 time,
2036 tz,
2037 msg,
2038 });
2039 }
2040 GIT_STATE_RECORD_REMOTE => {
2041 let flags = take_u8(&mut b)?;
2042 let name = take_str(&mut b)?;
2043 let fetch_url = take_str(&mut b)?;
2044 let push_url = take_str(&mut b)?;
2045 return Some(GitStateRecord::Remote {
2046 flags,
2047 name,
2048 fetch_url,
2049 push_url,
2050 });
2051 }
2052 _ => continue, }
2054 }
2055 }
2056}
2057
2058#[derive(Clone, Debug, PartialEq, Eq)]
2060pub enum GitCommitRecord<'a> {
2061 Commit {
2066 flags: u8,
2067 oid: GitOid,
2068 tree: GitOid,
2069 parents: Vec<GitOid>,
2070 author_time: i64,
2071 author_tz: i16,
2072 committer_time: i64,
2073 committer_tz: i16,
2074 author_name: &'a str,
2075 author_email: &'a str,
2076 committer_name: &'a str,
2077 committer_email: &'a str,
2078 message: &'a str,
2079 },
2080 PathAt {
2084 otype: u8,
2085 mode: u32,
2086 oid: GitOid,
2087 path: &'a str,
2088 },
2089}
2090
2091pub fn append_git_commit_record(buf: &mut Vec<u8>, record: &GitCommitRecord<'_>) {
2093 let start = begin_record(buf);
2094 match record {
2095 GitCommitRecord::Commit {
2096 flags,
2097 oid,
2098 tree,
2099 parents,
2100 author_time,
2101 author_tz,
2102 committer_time,
2103 committer_tz,
2104 author_name,
2105 author_email,
2106 committer_name,
2107 committer_email,
2108 message,
2109 } => {
2110 buf.push(GIT_COMMIT_RECORD_COMMIT);
2111 buf.push(*flags);
2112 buf.extend_from_slice(oid);
2113 buf.extend_from_slice(tree);
2114 buf.push(parents.len() as u8);
2115 push_oids(buf, parents);
2116 buf.extend_from_slice(&author_time.to_le_bytes());
2117 buf.extend_from_slice(&author_tz.to_le_bytes());
2118 buf.extend_from_slice(&committer_time.to_le_bytes());
2119 buf.extend_from_slice(&committer_tz.to_le_bytes());
2120 push_str(buf, author_name);
2121 push_str(buf, author_email);
2122 push_str(buf, committer_name);
2123 push_str(buf, committer_email);
2124 push_bytes(buf, message.as_bytes());
2125 }
2126 GitCommitRecord::PathAt {
2127 otype,
2128 mode,
2129 oid,
2130 path,
2131 } => {
2132 buf.push(GIT_COMMIT_RECORD_PATH_AT);
2133 buf.push(*otype);
2134 buf.extend_from_slice(&mode.to_le_bytes());
2135 buf.extend_from_slice(oid);
2136 push_str(buf, path);
2137 }
2138 }
2139 end_record(buf, start);
2140}
2141
2142pub struct GitCommitRecordIter<'a> {
2143 data: &'a [u8],
2144}
2145
2146pub fn git_commit_records(data: &[u8]) -> GitCommitRecordIter<'_> {
2148 GitCommitRecordIter { data }
2149}
2150
2151impl<'a> Iterator for GitCommitRecordIter<'a> {
2152 type Item = GitCommitRecord<'a>;
2153
2154 fn next(&mut self) -> Option<GitCommitRecord<'a>> {
2155 loop {
2156 let (kind, mut b) = next_record(&mut self.data)?;
2157 match kind {
2158 GIT_COMMIT_RECORD_COMMIT => {
2159 let flags = take_u8(&mut b)?;
2160 let oid = take_oid(&mut b)?;
2161 let tree = take_oid(&mut b)?;
2162 let n_parents = take_u8(&mut b)? as usize;
2163 let parents = take_oids(&mut b, n_parents)?;
2164 let author_time = take_i64(&mut b)?;
2165 let author_tz = take_i16(&mut b)?;
2166 let committer_time = take_i64(&mut b)?;
2167 let committer_tz = take_i16(&mut b)?;
2168 let author_name = take_str(&mut b)?;
2169 let author_email = take_str(&mut b)?;
2170 let committer_name = take_str(&mut b)?;
2171 let committer_email = take_str(&mut b)?;
2172 let message = std::str::from_utf8(take_bytes(&mut b)?).ok()?;
2173 return Some(GitCommitRecord::Commit {
2174 flags,
2175 oid,
2176 tree,
2177 parents,
2178 author_time,
2179 author_tz,
2180 committer_time,
2181 committer_tz,
2182 author_name,
2183 author_email,
2184 committer_name,
2185 committer_email,
2186 message,
2187 });
2188 }
2189 GIT_COMMIT_RECORD_PATH_AT => {
2190 let otype = take_u8(&mut b)?;
2191 let mode = take_u32(&mut b)?;
2192 let oid = take_oid(&mut b)?;
2193 let path = take_str(&mut b)?;
2194 return Some(GitCommitRecord::PathAt {
2195 otype,
2196 mode,
2197 oid,
2198 path,
2199 });
2200 }
2201 _ => continue, }
2203 }
2204 }
2205}
2206
2207#[derive(Clone, Debug, PartialEq, Eq)]
2209pub enum GitTreeRecord<'a> {
2210 Entry {
2213 otype: u8,
2214 mode: u32,
2215 oid: GitOid,
2216 name: &'a str,
2217 },
2218 Cursor { after: &'a str, pos: u64 },
2220}
2221
2222pub fn append_git_tree_record(buf: &mut Vec<u8>, record: &GitTreeRecord<'_>) {
2224 if let GitTreeRecord::Cursor { after, pos } = record {
2225 push_cursor_record(buf, after, *pos);
2226 return;
2227 }
2228 let start = begin_record(buf);
2229 match record {
2230 GitTreeRecord::Entry {
2231 otype,
2232 mode,
2233 oid,
2234 name,
2235 } => {
2236 buf.push(GIT_TREE_RECORD_ENTRY);
2237 buf.push(*otype);
2238 buf.extend_from_slice(&mode.to_le_bytes());
2239 buf.extend_from_slice(oid);
2240 push_str(buf, name);
2241 }
2242 GitTreeRecord::Cursor { .. } => unreachable!("handled above"),
2243 }
2244 end_record(buf, start);
2245}
2246
2247pub struct GitTreeRecordIter<'a> {
2248 data: &'a [u8],
2249}
2250
2251pub fn git_tree_records(data: &[u8]) -> GitTreeRecordIter<'_> {
2253 GitTreeRecordIter { data }
2254}
2255
2256impl<'a> Iterator for GitTreeRecordIter<'a> {
2257 type Item = GitTreeRecord<'a>;
2258
2259 fn next(&mut self) -> Option<GitTreeRecord<'a>> {
2260 loop {
2261 let (kind, mut b) = next_record(&mut self.data)?;
2262 match kind {
2263 GIT_TREE_RECORD_ENTRY => {
2264 let otype = take_u8(&mut b)?;
2265 let mode = take_u32(&mut b)?;
2266 let oid = take_oid(&mut b)?;
2267 let name = take_str(&mut b)?;
2268 return Some(GitTreeRecord::Entry {
2269 otype,
2270 mode,
2271 oid,
2272 name,
2273 });
2274 }
2275 GIT_RECORD_CURSOR => {
2276 let (after, pos) = take_cursor(&mut b)?;
2277 return Some(GitTreeRecord::Cursor { after, pos });
2278 }
2279 _ => continue, }
2281 }
2282 }
2283}
2284
2285#[derive(Clone, Debug, PartialEq, Eq)]
2287pub enum GitDiffRecord<'a> {
2288 Entry {
2294 st: u8,
2295 similarity: u8,
2296 dflags: u8,
2297 old_mode: u32,
2298 new_mode: u32,
2299 old_oid: GitOid,
2300 new_oid: GitOid,
2301 old_path: &'a str,
2302 new_path: &'a str,
2303 },
2304 Base { oid: GitOid },
2307 Cursor { after: &'a str, pos: u64 },
2309}
2310
2311pub fn append_git_diff_record(buf: &mut Vec<u8>, record: &GitDiffRecord<'_>) {
2313 if let GitDiffRecord::Cursor { after, pos } = record {
2314 push_cursor_record(buf, after, *pos);
2315 return;
2316 }
2317 let start = begin_record(buf);
2318 match record {
2319 GitDiffRecord::Entry {
2320 st,
2321 similarity,
2322 dflags,
2323 old_mode,
2324 new_mode,
2325 old_oid,
2326 new_oid,
2327 old_path,
2328 new_path,
2329 } => {
2330 buf.push(GIT_DIFF_RECORD_ENTRY);
2331 buf.push(*st);
2332 buf.push(*similarity);
2333 buf.push(*dflags);
2334 buf.extend_from_slice(&old_mode.to_le_bytes());
2335 buf.extend_from_slice(&new_mode.to_le_bytes());
2336 buf.extend_from_slice(old_oid);
2337 buf.extend_from_slice(new_oid);
2338 push_str(buf, old_path);
2339 push_str(buf, new_path);
2340 }
2341 GitDiffRecord::Base { oid } => {
2342 buf.push(GIT_DIFF_RECORD_BASE);
2343 buf.extend_from_slice(oid);
2344 }
2345 GitDiffRecord::Cursor { .. } => unreachable!("handled above"),
2346 }
2347 end_record(buf, start);
2348}
2349
2350pub struct GitDiffRecordIter<'a> {
2351 data: &'a [u8],
2352}
2353
2354pub fn git_diff_records(data: &[u8]) -> GitDiffRecordIter<'_> {
2356 GitDiffRecordIter { data }
2357}
2358
2359impl<'a> Iterator for GitDiffRecordIter<'a> {
2360 type Item = GitDiffRecord<'a>;
2361
2362 fn next(&mut self) -> Option<GitDiffRecord<'a>> {
2363 loop {
2364 let (kind, mut b) = next_record(&mut self.data)?;
2365 match kind {
2366 GIT_DIFF_RECORD_ENTRY => {
2367 let st = take_u8(&mut b)?;
2368 let similarity = take_u8(&mut b)?;
2369 let dflags = take_u8(&mut b)?;
2370 let old_mode = take_u32(&mut b)?;
2371 let new_mode = take_u32(&mut b)?;
2372 let old_oid = take_oid(&mut b)?;
2373 let new_oid = take_oid(&mut b)?;
2374 let old_path = take_str(&mut b)?;
2375 let new_path = take_str(&mut b)?;
2376 return Some(GitDiffRecord::Entry {
2377 st,
2378 similarity,
2379 dflags,
2380 old_mode,
2381 new_mode,
2382 old_oid,
2383 new_oid,
2384 old_path,
2385 new_path,
2386 });
2387 }
2388 GIT_DIFF_RECORD_BASE => {
2389 let oid = take_oid(&mut b)?;
2390 return Some(GitDiffRecord::Base { oid });
2391 }
2392 GIT_RECORD_CURSOR => {
2393 let (after, pos) = take_cursor(&mut b)?;
2394 return Some(GitDiffRecord::Cursor { after, pos });
2395 }
2396 _ => continue, }
2398 }
2399 }
2400}
2401
2402#[derive(Clone, Debug, PartialEq, Eq)]
2404pub enum GitPatchRecord<'a> {
2405 File {
2414 st: u8,
2415 similarity: u8,
2416 flags: u8,
2417 old_path: &'a str,
2418 new_path: &'a str,
2419 },
2420 Row {
2426 old_line: u32,
2427 new_line: u32,
2428 old_text: &'a [u8],
2429 new_text: &'a [u8],
2430 old_spans: Vec<(u32, u32)>,
2431 new_spans: Vec<(u32, u32)>,
2432 },
2433 Gap { old_line: u32, new_line: u32 },
2436 Base { oid: GitOid },
2438 Cursor { after: &'a str, pos: u64 },
2442}
2443
2444fn push_spans(buf: &mut Vec<u8>, spans: &[(u32, u32)]) {
2445 buf.extend_from_slice(&(spans.len() as u16).to_le_bytes());
2446 for (start, len) in spans {
2447 buf.extend_from_slice(&start.to_le_bytes());
2448 buf.extend_from_slice(&len.to_le_bytes());
2449 }
2450}
2451
2452fn take_spans(b: &mut &[u8]) -> Option<Vec<(u32, u32)>> {
2453 let n = take_u16(b)? as usize;
2454 if b.len() < n * 8 {
2455 return None;
2456 }
2457 let mut spans = Vec::with_capacity(n);
2458 for _ in 0..n {
2459 let start = take_u32(b)?;
2460 let len = take_u32(b)?;
2461 spans.push((start, len));
2462 }
2463 Some(spans)
2464}
2465
2466pub fn append_git_patch_record(buf: &mut Vec<u8>, record: &GitPatchRecord<'_>) {
2468 if let GitPatchRecord::Cursor { after, pos } = record {
2469 push_cursor_record(buf, after, *pos);
2470 return;
2471 }
2472 let start = begin_record(buf);
2473 match record {
2474 GitPatchRecord::File {
2475 st,
2476 similarity,
2477 flags,
2478 old_path,
2479 new_path,
2480 } => {
2481 buf.push(GIT_PATCH_RECORD_FILE);
2482 buf.push(*st);
2483 buf.push(*similarity);
2484 buf.push(*flags);
2485 push_str(buf, old_path);
2486 push_str(buf, new_path);
2487 }
2488 GitPatchRecord::Row {
2489 old_line,
2490 new_line,
2491 old_text,
2492 new_text,
2493 old_spans,
2494 new_spans,
2495 } => {
2496 buf.push(GIT_PATCH_RECORD_ROW);
2497 buf.extend_from_slice(&old_line.to_le_bytes());
2498 buf.extend_from_slice(&new_line.to_le_bytes());
2499 push_bytes(buf, old_text);
2500 push_bytes(buf, new_text);
2501 push_spans(buf, old_spans);
2502 push_spans(buf, new_spans);
2503 }
2504 GitPatchRecord::Gap { old_line, new_line } => {
2505 buf.push(GIT_PATCH_RECORD_GAP);
2506 buf.extend_from_slice(&old_line.to_le_bytes());
2507 buf.extend_from_slice(&new_line.to_le_bytes());
2508 }
2509 GitPatchRecord::Base { oid } => {
2510 buf.push(GIT_PATCH_RECORD_BASE);
2511 buf.extend_from_slice(oid);
2512 }
2513 GitPatchRecord::Cursor { .. } => unreachable!("handled above"),
2514 }
2515 end_record(buf, start);
2516}
2517
2518pub struct GitPatchRecordIter<'a> {
2519 data: &'a [u8],
2520}
2521
2522pub fn git_patch_records(data: &[u8]) -> GitPatchRecordIter<'_> {
2524 GitPatchRecordIter { data }
2525}
2526
2527impl<'a> Iterator for GitPatchRecordIter<'a> {
2528 type Item = GitPatchRecord<'a>;
2529
2530 fn next(&mut self) -> Option<GitPatchRecord<'a>> {
2531 loop {
2532 let (kind, mut b) = next_record(&mut self.data)?;
2533 match kind {
2534 GIT_PATCH_RECORD_FILE => {
2535 let st = take_u8(&mut b)?;
2536 let similarity = take_u8(&mut b)?;
2537 let flags = take_u8(&mut b)?;
2538 let old_path = take_str(&mut b)?;
2539 let new_path = take_str(&mut b)?;
2540 return Some(GitPatchRecord::File {
2541 st,
2542 similarity,
2543 flags,
2544 old_path,
2545 new_path,
2546 });
2547 }
2548 GIT_PATCH_RECORD_ROW => {
2549 let old_line = take_u32(&mut b)?;
2550 let new_line = take_u32(&mut b)?;
2551 let old_text = take_bytes(&mut b)?;
2552 let new_text = take_bytes(&mut b)?;
2553 let old_spans = take_spans(&mut b)?;
2554 let new_spans = take_spans(&mut b)?;
2555 return Some(GitPatchRecord::Row {
2556 old_line,
2557 new_line,
2558 old_text,
2559 new_text,
2560 old_spans,
2561 new_spans,
2562 });
2563 }
2564 GIT_PATCH_RECORD_GAP => {
2565 let old_line = take_u32(&mut b)?;
2566 let new_line = take_u32(&mut b)?;
2567 return Some(GitPatchRecord::Gap { old_line, new_line });
2568 }
2569 GIT_RECORD_CURSOR => {
2570 let (after, pos) = take_cursor(&mut b)?;
2571 return Some(GitPatchRecord::Cursor { after, pos });
2572 }
2573 GIT_PATCH_RECORD_BASE => {
2574 let oid = take_oid(&mut b)?;
2575 return Some(GitPatchRecord::Base { oid });
2576 }
2577 _ => continue, }
2579 }
2580 }
2581}
2582
2583#[derive(Clone, Debug, PartialEq, Eq)]
2585pub enum GitIndexRecord<'a> {
2586 Entry {
2589 stage: u8,
2590 iflags: u8,
2591 mode: u32,
2592 size: u64,
2593 mtime_ns: u64,
2594 oid: GitOid,
2595 path: &'a str,
2596 },
2597 Cursor { after: &'a str, pos: u64 },
2599}
2600
2601pub fn append_git_index_record(buf: &mut Vec<u8>, record: &GitIndexRecord<'_>) {
2603 if let GitIndexRecord::Cursor { after, pos } = record {
2604 push_cursor_record(buf, after, *pos);
2605 return;
2606 }
2607 let start = begin_record(buf);
2608 match record {
2609 GitIndexRecord::Entry {
2610 stage,
2611 iflags,
2612 mode,
2613 size,
2614 mtime_ns,
2615 oid,
2616 path,
2617 } => {
2618 buf.push(GIT_INDEX_RECORD_ENTRY);
2619 buf.push(*stage);
2620 buf.push(*iflags);
2621 buf.extend_from_slice(&mode.to_le_bytes());
2622 buf.extend_from_slice(&size.to_le_bytes());
2623 buf.extend_from_slice(&mtime_ns.to_le_bytes());
2624 buf.extend_from_slice(oid);
2625 push_str(buf, path);
2626 }
2627 GitIndexRecord::Cursor { .. } => unreachable!("handled above"),
2628 }
2629 end_record(buf, start);
2630}
2631
2632pub struct GitIndexRecordIter<'a> {
2633 data: &'a [u8],
2634}
2635
2636pub fn git_index_records(data: &[u8]) -> GitIndexRecordIter<'_> {
2638 GitIndexRecordIter { data }
2639}
2640
2641impl<'a> Iterator for GitIndexRecordIter<'a> {
2642 type Item = GitIndexRecord<'a>;
2643
2644 fn next(&mut self) -> Option<GitIndexRecord<'a>> {
2645 loop {
2646 let (kind, mut b) = next_record(&mut self.data)?;
2647 match kind {
2648 GIT_INDEX_RECORD_ENTRY => {
2649 let stage = take_u8(&mut b)?;
2650 let iflags = take_u8(&mut b)?;
2651 let mode = take_u32(&mut b)?;
2652 let size = take_u64(&mut b)?;
2653 let mtime_ns = take_u64(&mut b)?;
2654 let oid = take_oid(&mut b)?;
2655 let path = take_str(&mut b)?;
2656 return Some(GitIndexRecord::Entry {
2657 stage,
2658 iflags,
2659 mode,
2660 size,
2661 mtime_ns,
2662 oid,
2663 path,
2664 });
2665 }
2666 GIT_RECORD_CURSOR => {
2667 let (after, pos) = take_cursor(&mut b)?;
2668 return Some(GitIndexRecord::Cursor { after, pos });
2669 }
2670 _ => continue, }
2672 }
2673 }
2674}
2675
2676macro_rules! records_resp {
2681 ($build:ident, $parse:ident, $opcode:ident) => {
2682 pub fn $build(nonce: u16, status: u8, flags: u8, records: &[u8]) -> Vec<u8> {
2684 let compressed = lz4_flex::compress_prepend_size(records);
2685 let mut msg = Vec::with_capacity(5 + compressed.len());
2686 msg.push($opcode);
2687 msg.extend_from_slice(&nonce.to_le_bytes());
2688 msg.push(status);
2689 msg.push(flags);
2690 msg.extend_from_slice(&compressed);
2691 msg
2692 }
2693
2694 pub fn $parse(msg: &[u8]) -> Option<(u16, u8, u8, Vec<u8>)> {
2696 let mut b = body_of(msg, $opcode)?;
2697 let nonce = take_u16(&mut b)?;
2698 let status = take_u8(&mut b)?;
2699 let flags = take_u8(&mut b)?;
2700 let records = decompress_guarded(b)?;
2701 Some((nonce, status, flags, records))
2702 }
2703 };
2704}
2705
2706records_resp!(
2707 msg_git_discover_resp,
2708 parse_git_discover_resp,
2709 S2C_GIT_DISCOVER
2710);
2711records_resp!(msg_git_blame_resp, parse_git_blame_resp, S2C_GIT_BLAME);
2712records_resp!(msg_git_reflog_resp, parse_git_reflog_resp, S2C_GIT_REFLOG);
2713records_resp!(msg_git_fetch_resp, parse_git_fetch_resp, S2C_GIT_FETCH);
2714
2715#[derive(Clone, Debug, PartialEq, Eq)]
2717pub enum GitDiscoverRecord<'a> {
2718 Repo {
2722 flags: u8,
2723 workdir: &'a str,
2724 gitdir: &'a str,
2725 },
2726 Cursor {
2727 after: &'a str,
2728 pos: u64,
2729 },
2730}
2731
2732pub fn append_git_discover_record(buf: &mut Vec<u8>, record: &GitDiscoverRecord<'_>) {
2733 match record {
2734 GitDiscoverRecord::Cursor { after, pos } => push_cursor_record(buf, after, *pos),
2735 GitDiscoverRecord::Repo {
2736 flags,
2737 workdir,
2738 gitdir,
2739 } => {
2740 let start = begin_record(buf);
2741 buf.push(GIT_DISCOVER_RECORD_REPO);
2742 buf.push(*flags);
2743 push_str(buf, workdir);
2744 push_str(buf, gitdir);
2745 end_record(buf, start);
2746 }
2747 }
2748}
2749
2750pub struct GitDiscoverRecordIter<'a> {
2751 data: &'a [u8],
2752}
2753
2754pub fn git_discover_records(data: &[u8]) -> GitDiscoverRecordIter<'_> {
2755 GitDiscoverRecordIter { data }
2756}
2757
2758impl<'a> Iterator for GitDiscoverRecordIter<'a> {
2759 type Item = GitDiscoverRecord<'a>;
2760
2761 fn next(&mut self) -> Option<GitDiscoverRecord<'a>> {
2762 loop {
2763 let (kind, mut b) = next_record(&mut self.data)?;
2764 match kind {
2765 GIT_DISCOVER_RECORD_REPO => {
2766 let flags = take_u8(&mut b)?;
2767 let workdir = take_str(&mut b)?;
2768 let gitdir = take_str(&mut b)?;
2769 return Some(GitDiscoverRecord::Repo {
2770 flags,
2771 workdir,
2772 gitdir,
2773 });
2774 }
2775 GIT_RECORD_CURSOR => {
2776 let (after, pos) = take_cursor(&mut b)?;
2777 return Some(GitDiscoverRecord::Cursor { after, pos });
2778 }
2779 _ => continue,
2780 }
2781 }
2782 }
2783}
2784
2785#[derive(Clone, Debug, PartialEq, Eq)]
2787pub enum GitBlameRecord<'a> {
2788 Range {
2794 flags: u8,
2795 commit: GitOid,
2796 start_line: u32,
2797 line_count: u32,
2798 orig_start: u32,
2799 orig_path: &'a str,
2801 },
2802 Cursor {
2803 after: &'a str,
2804 pos: u64,
2805 },
2806}
2807
2808pub fn append_git_blame_record(buf: &mut Vec<u8>, record: &GitBlameRecord<'_>) {
2809 match record {
2810 GitBlameRecord::Cursor { after, pos } => push_cursor_record(buf, after, *pos),
2811 GitBlameRecord::Range {
2812 flags,
2813 commit,
2814 start_line,
2815 line_count,
2816 orig_start,
2817 orig_path,
2818 } => {
2819 let start = begin_record(buf);
2820 buf.push(GIT_BLAME_RECORD_RANGE);
2821 buf.push(*flags);
2822 buf.extend_from_slice(commit);
2823 buf.extend_from_slice(&start_line.to_le_bytes());
2824 buf.extend_from_slice(&line_count.to_le_bytes());
2825 buf.extend_from_slice(&orig_start.to_le_bytes());
2826 push_str(buf, orig_path);
2827 end_record(buf, start);
2828 }
2829 }
2830}
2831
2832pub struct GitBlameRecordIter<'a> {
2833 data: &'a [u8],
2834}
2835
2836pub fn git_blame_records(data: &[u8]) -> GitBlameRecordIter<'_> {
2837 GitBlameRecordIter { data }
2838}
2839
2840impl<'a> Iterator for GitBlameRecordIter<'a> {
2841 type Item = GitBlameRecord<'a>;
2842
2843 fn next(&mut self) -> Option<GitBlameRecord<'a>> {
2844 loop {
2845 let (kind, mut b) = next_record(&mut self.data)?;
2846 match kind {
2847 GIT_BLAME_RECORD_RANGE => {
2848 let flags = take_u8(&mut b)?;
2849 let commit = take_oid(&mut b)?;
2850 let start_line = take_u32(&mut b)?;
2851 let line_count = take_u32(&mut b)?;
2852 let orig_start = take_u32(&mut b)?;
2853 let orig_path = take_str(&mut b)?;
2854 return Some(GitBlameRecord::Range {
2855 flags,
2856 commit,
2857 start_line,
2858 line_count,
2859 orig_start,
2860 orig_path,
2861 });
2862 }
2863 GIT_RECORD_CURSOR => {
2864 let (after, pos) = take_cursor(&mut b)?;
2865 return Some(GitBlameRecord::Cursor { after, pos });
2866 }
2867 _ => continue,
2868 }
2869 }
2870 }
2871}
2872
2873#[derive(Clone, Debug, PartialEq, Eq)]
2875pub enum GitReflogRecord<'a> {
2876 Entry {
2880 flags: u8,
2881 old: GitOid,
2882 new: GitOid,
2883 time: i64,
2884 tz: i16,
2885 msg: &'a str,
2886 },
2887 Cursor {
2888 after: &'a str,
2889 pos: u64,
2890 },
2891}
2892
2893pub fn append_git_reflog_record(buf: &mut Vec<u8>, record: &GitReflogRecord<'_>) {
2894 match record {
2895 GitReflogRecord::Cursor { after, pos } => push_cursor_record(buf, after, *pos),
2896 GitReflogRecord::Entry {
2897 flags,
2898 old,
2899 new,
2900 time,
2901 tz,
2902 msg,
2903 } => {
2904 let start = begin_record(buf);
2905 buf.push(GIT_REFLOG_RECORD_ENTRY);
2906 buf.push(*flags);
2907 buf.extend_from_slice(old);
2908 buf.extend_from_slice(new);
2909 buf.extend_from_slice(&time.to_le_bytes());
2910 buf.extend_from_slice(&tz.to_le_bytes());
2911 push_str(buf, msg);
2912 end_record(buf, start);
2913 }
2914 }
2915}
2916
2917pub struct GitReflogRecordIter<'a> {
2918 data: &'a [u8],
2919}
2920
2921pub fn git_reflog_records(data: &[u8]) -> GitReflogRecordIter<'_> {
2922 GitReflogRecordIter { data }
2923}
2924
2925impl<'a> Iterator for GitReflogRecordIter<'a> {
2926 type Item = GitReflogRecord<'a>;
2927
2928 fn next(&mut self) -> Option<GitReflogRecord<'a>> {
2929 loop {
2930 let (kind, mut b) = next_record(&mut self.data)?;
2931 match kind {
2932 GIT_REFLOG_RECORD_ENTRY => {
2933 let flags = take_u8(&mut b)?;
2934 let old = take_oid(&mut b)?;
2935 let new = take_oid(&mut b)?;
2936 let time = take_i64(&mut b)?;
2937 let tz = take_i16(&mut b)?;
2938 let msg = take_str(&mut b)?;
2939 return Some(GitReflogRecord::Entry {
2940 flags,
2941 old,
2942 new,
2943 time,
2944 tz,
2945 msg,
2946 });
2947 }
2948 GIT_RECORD_CURSOR => {
2949 let (after, pos) = take_cursor(&mut b)?;
2950 return Some(GitReflogRecord::Cursor { after, pos });
2951 }
2952 _ => continue,
2953 }
2954 }
2955 }
2956}
2957
2958#[derive(Clone, Debug, PartialEq, Eq)]
2960pub enum GitFetchRecord<'a> {
2961 Ref {
2967 flags: u8,
2968 status: u8,
2969 old: GitOid,
2970 new: GitOid,
2971 name: &'a str,
2972 detail: &'a str,
2973 },
2974}
2975
2976pub fn append_git_fetch_record(buf: &mut Vec<u8>, record: &GitFetchRecord<'_>) {
2977 let start = begin_record(buf);
2978 match record {
2979 GitFetchRecord::Ref {
2980 flags,
2981 status,
2982 old,
2983 new,
2984 name,
2985 detail,
2986 } => {
2987 buf.push(GIT_FETCH_RECORD_REF);
2988 buf.push(*flags);
2989 buf.push(*status);
2990 buf.extend_from_slice(old);
2991 buf.extend_from_slice(new);
2992 push_str(buf, name);
2993 push_str(buf, detail);
2994 }
2995 }
2996 end_record(buf, start);
2997}
2998
2999pub struct GitFetchRecordIter<'a> {
3000 data: &'a [u8],
3001}
3002
3003pub fn git_fetch_records(data: &[u8]) -> GitFetchRecordIter<'_> {
3004 GitFetchRecordIter { data }
3005}
3006
3007impl<'a> Iterator for GitFetchRecordIter<'a> {
3008 type Item = GitFetchRecord<'a>;
3009
3010 fn next(&mut self) -> Option<GitFetchRecord<'a>> {
3011 loop {
3012 let (kind, mut b) = next_record(&mut self.data)?;
3013 match kind {
3014 GIT_FETCH_RECORD_REF => {
3015 let flags = take_u8(&mut b)?;
3016 let status = take_u8(&mut b)?;
3017 let old = take_oid(&mut b)?;
3018 let new = take_oid(&mut b)?;
3019 let name = take_str(&mut b)?;
3020 let detail = take_str(&mut b)?;
3021 return Some(GitFetchRecord::Ref {
3022 flags,
3023 status,
3024 old,
3025 new,
3026 name,
3027 detail,
3028 });
3029 }
3030 _ => continue,
3031 }
3032 }
3033 }
3034}
3035
3036#[derive(Clone, Debug, Default, PartialEq, Eq)]
3042pub struct GitHead {
3043 pub flags: u8,
3044 pub oid: GitOid,
3045 pub name: String,
3047}
3048
3049#[derive(Clone, Debug, Default, PartialEq, Eq)]
3051pub struct GitRefState {
3052 pub flags: u8,
3053 pub oid: GitOid,
3054 pub peeled: GitOid,
3056 pub target: String,
3060}
3061
3062#[derive(Clone, Debug, Default, PartialEq, Eq)]
3064pub struct GitRemoteState {
3065 pub flags: u8,
3066 pub fetch_url: String,
3068 pub push_url: String,
3070}
3071
3072#[derive(Clone, Debug, Default, PartialEq, Eq)]
3074pub struct GitOpState {
3075 pub op: u8,
3076 pub oid: GitOid,
3077 pub detail: String,
3078}
3079
3080#[derive(Clone, Debug, Default, PartialEq, Eq)]
3082pub struct GitStatusEntry {
3083 pub staged: u8,
3084 pub unstaged: u8,
3085 pub flags: u8,
3086 pub oid: GitOid,
3089 pub old_path: String,
3091 pub path: String,
3092}
3093
3094#[derive(Clone, Debug, Default, PartialEq, Eq)]
3097pub struct GitUpstreamState {
3098 pub flags: u8,
3099 pub ahead: u32,
3100 pub behind: u32,
3101 pub upstream: String,
3102}
3103
3104#[derive(Clone, Debug, Default, PartialEq, Eq)]
3106pub struct GitStashEntry {
3107 pub index: u16,
3109 pub oid: GitOid,
3110 pub time: i64,
3111 pub tz: i16,
3112 pub message: String,
3113}
3114
3115#[derive(Clone, Debug, Default, PartialEq, Eq)]
3119pub struct GitStateMirror {
3120 pub head: Option<GitHead>,
3121 pub refs: BTreeMap<String, GitRefState>,
3123 pub op: Option<GitOpState>,
3124 pub status: Vec<GitStatusEntry>,
3125 pub upstreams: BTreeMap<String, GitUpstreamState>,
3127 pub stashes: Vec<GitStashEntry>,
3128 pub remotes: BTreeMap<String, GitRemoteState>,
3130 pub flags: u8,
3132 pending: Vec<u8>,
3136 pending_state_id: u32,
3137}
3138
3139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3141pub enum GitStateApply {
3142 Complete(u32),
3144 Partial,
3147 Malformed,
3149}
3150
3151impl GitStateApply {
3152 pub fn complete(self) -> Option<u32> {
3156 match self {
3157 GitStateApply::Complete(state_id) => Some(state_id),
3158 GitStateApply::Partial | GitStateApply::Malformed => None,
3159 }
3160 }
3161}
3162
3163impl GitStateMirror {
3164 pub fn new() -> Self {
3165 Self::default()
3166 }
3167
3168 pub fn apply_state(&mut self, msg: &[u8]) -> GitStateApply {
3173 let Some((_repo_id, state_id, flags, records)) = parse_git_state(msg) else {
3174 self.pending.clear();
3175 return GitStateApply::Malformed;
3176 };
3177 if state_id != self.pending_state_id {
3180 self.pending.clear();
3181 self.pending_state_id = state_id;
3182 }
3183 if flags & GIT_STATE_PARTIAL != 0 {
3184 self.pending.extend_from_slice(&records);
3185 return GitStateApply::Partial;
3186 }
3187 let records = if self.pending.is_empty() {
3188 records
3189 } else {
3190 let mut whole = std::mem::take(&mut self.pending);
3191 whole.extend_from_slice(&records);
3192 whole
3193 };
3194 if self.apply_records(flags, &records) {
3195 GitStateApply::Complete(state_id)
3196 } else {
3197 GitStateApply::Malformed
3198 }
3199 }
3200
3201 fn apply_records(&mut self, flags: u8, records: &[u8]) -> bool {
3205 let mut next = GitStateMirror {
3206 flags,
3207 ..Default::default()
3208 };
3209 for record in git_state_records(records) {
3210 match record {
3211 GitStateRecord::Head { flags, oid, name } => {
3212 next.head = Some(GitHead {
3213 flags,
3214 oid,
3215 name: name.to_string(),
3216 });
3217 }
3218 GitStateRecord::Ref {
3219 flags,
3220 oid,
3221 peeled,
3222 name,
3223 target,
3224 } => {
3225 next.refs.insert(
3226 name.to_string(),
3227 GitRefState {
3228 flags,
3229 oid,
3230 peeled,
3231 target: target.to_string(),
3232 },
3233 );
3234 }
3235 GitStateRecord::Op { op, oid, detail } => {
3236 next.op = Some(GitOpState {
3237 op,
3238 oid,
3239 detail: detail.to_string(),
3240 });
3241 }
3242 GitStateRecord::Status {
3243 staged,
3244 unstaged,
3245 flags,
3246 oid,
3247 old_path,
3248 path,
3249 } => {
3250 next.status.push(GitStatusEntry {
3251 staged,
3252 unstaged,
3253 flags,
3254 oid,
3255 old_path: old_path.to_string(),
3256 path: path.to_string(),
3257 });
3258 }
3259 GitStateRecord::Upstream {
3260 flags,
3261 ahead,
3262 behind,
3263 name,
3264 upstream,
3265 } => {
3266 next.upstreams.insert(
3267 name.to_string(),
3268 GitUpstreamState {
3269 flags,
3270 ahead,
3271 behind,
3272 upstream: upstream.to_string(),
3273 },
3274 );
3275 }
3276 GitStateRecord::Stash {
3277 index,
3278 oid,
3279 time,
3280 tz,
3281 msg,
3282 } => {
3283 next.stashes.push(GitStashEntry {
3284 index,
3285 oid,
3286 time,
3287 tz,
3288 message: msg.to_string(),
3289 });
3290 }
3291 GitStateRecord::Remote {
3292 flags,
3293 name,
3294 fetch_url,
3295 push_url,
3296 } => {
3297 next.remotes.insert(
3298 name.to_string(),
3299 GitRemoteState {
3300 flags,
3301 fetch_url: fetch_url.to_string(),
3302 push_url: push_url.to_string(),
3303 },
3304 );
3305 }
3306 }
3307 }
3308 *self = next;
3309 true
3310 }
3311}
3312
3313#[cfg(test)]
3314mod tests {
3315 use super::*;
3316
3317 #[test]
3323 fn overlong_strings_are_clipped_not_wrapped() {
3324 let mut buf = Vec::new();
3325 let long = "a".repeat(MAX_STR + 1);
3327 push_str(&mut buf, &long);
3328 let declared = u16::from_le_bytes([buf[0], buf[1]]) as usize;
3329 assert_eq!(declared, MAX_STR);
3330 assert_eq!(buf.len(), 2 + declared, "prefix must match the bytes");
3331
3332 let mut buf = Vec::new();
3335 let wide = "é".repeat(MAX_STR);
3336 push_str(&mut buf, &wide);
3337 let declared = u16::from_le_bytes([buf[0], buf[1]]) as usize;
3338 assert!(declared <= MAX_STR);
3339 assert_eq!(buf.len(), 2 + declared);
3340 std::str::from_utf8(&buf[2..]).expect("clipped field is still UTF-8");
3341 }
3342
3343 #[test]
3344 fn git_open_pty_context_and_rebase() {
3345 let m = msg_git_open(&GitOpenRequest {
3346 src_pty_id: 9,
3347 ..GitOpenRequest::new(1, GIT_OPEN_WATCH, "sub")
3348 });
3349 let req = parse_git_open(&m).unwrap();
3350 assert_eq!(req.src_pty_id, 9);
3351 assert_eq!(req.parent_repo_id, GIT_OPEN_NO_CONTEXT);
3352 assert_eq!(req.path, "sub");
3353 let reb = git_open_rebase(&m, Some("/w")).unwrap();
3356 let rebased = parse_git_open(&reb).unwrap();
3357 assert_eq!(rebased.src_pty_id, GIT_OPEN_NO_CONTEXT);
3358 assert_eq!(rebased.path, "/w/sub");
3359 let plain = msg_git_open(&GitOpenRequest::new(1, GIT_OPEN_WATCH, "sub"));
3361 assert_eq!(git_open_rebase(&plain, Some("/w")), None);
3362 }
3363
3364 #[test]
3365 fn git_open_parent_repo_and_prefixes() {
3366 let m = msg_git_open(&GitOpenRequest {
3369 parent_repo_id: 4,
3370 ref_prefixes: vec!["refs/heads/", "refs/remotes/origin/"],
3371 ..GitOpenRequest::new(2, GIT_OPEN_WATCH | GIT_OPEN_REMOTES, "vendor/lib")
3372 });
3373 let req = parse_git_open(&m).unwrap();
3374 assert_eq!(req.parent_repo_id, 4);
3375 assert_eq!(req.src_pty_id, GIT_OPEN_NO_CONTEXT);
3376 assert_eq!(
3377 req.ref_prefixes,
3378 vec!["refs/heads/", "refs/remotes/origin/"]
3379 );
3380 assert_eq!(req.path, "vendor/lib");
3381 assert_eq!(req.flags & GIT_OPEN_REMOTES, GIT_OPEN_REMOTES);
3382 }
3383
3384 fn oid(fill: u8) -> GitOid {
3387 let mut o = [0u8; 32];
3388 o[..20].fill(fill);
3389 o
3390 }
3391
3392 fn hex(b: &[u8]) -> String {
3393 b.iter().map(|x| format!("{x:02x}")).collect()
3394 }
3395
3396 #[test]
3397 fn request_roundtrips() {
3398 let open = GitOpenRequest {
3399 refs_latency_ms: 50,
3400 status_latency_ms: 500,
3401 ..GitOpenRequest::new(1, GIT_OPEN_WATCH | GIT_OPEN_STATUS, "/repo")
3402 };
3403 assert_eq!(parse_git_open(&msg_git_open(&open)), Some(open));
3404 let bare = GitOpenRequest::new(0, 0, "");
3406 assert_eq!(parse_git_open(&msg_git_open(&bare)), Some(bare));
3407
3408 assert_eq!(parse_git_close(&msg_git_close(7)), Some(7));
3409 assert_eq!(
3410 parse_git_ack(&msg_git_ack(7, u32::MAX)),
3411 Some((7, u32::MAX))
3412 );
3413
3414 let tips = vec![oid(0xAA), oid(0xAB)];
3415 let hides = vec![oid(0xBB)];
3416 let msg = msg_git_log(
3417 3,
3418 7,
3419 GIT_LOG_FOLLOW | GIT_LOG_PATH_OIDS,
3420 100,
3421 "src/a.rs",
3422 &tips,
3423 &hides,
3424 );
3425 assert_eq!(
3426 parse_git_log(&msg),
3427 Some(GitLogRequest {
3428 nonce: 3,
3429 repo_id: 7,
3430 flags: GIT_LOG_FOLLOW | GIT_LOG_PATH_OIDS,
3431 limit: 100,
3432 path: "src/a.rs",
3433 tips,
3434 hides,
3435 })
3436 );
3437 let msg = msg_git_log(4, 7, 0, 0, "", &[], &[]);
3439 assert_eq!(
3440 parse_git_log(&msg),
3441 Some(GitLogRequest {
3442 nonce: 4,
3443 repo_id: 7,
3444 flags: 0,
3445 limit: 0,
3446 path: "",
3447 tips: vec![],
3448 hides: vec![],
3449 })
3450 );
3451
3452 let tree = GitTreeRequest {
3453 nonce: 4,
3454 repo_id: 7,
3455 flags: 0,
3456 oid: oid(0xCC),
3457 path: "dir/%FF",
3458 after: "dir/%FF/z.txt",
3459 };
3460 assert_eq!(parse_git_tree(&msg_git_tree(&tree)), Some(tree));
3461
3462 let blob = GitBlobRequest {
3464 nonce: 5,
3465 repo_id: 7,
3466 flags: 0,
3467 oid: oid(0xDD),
3468 path: "",
3469 offset: 1 << 24,
3470 max_len: 1 << 20,
3471 };
3472 assert_eq!(parse_git_blob(&msg_git_blob(&blob)), Some(blob));
3473
3474 let old = GitEndpoint {
3475 kind: GIT_ENDPOINT_COMMIT,
3476 oid: oid(0x11),
3477 };
3478 let new = GitEndpoint {
3479 kind: GIT_ENDPOINT_WORKTREE,
3480 oid: GIT_OID_NONE,
3481 };
3482 let diff = GitDiffRequest {
3483 nonce: 6,
3484 repo_id: 7,
3485 flags: GIT_DIFF_RENAMES,
3486 rename: 50,
3487 old,
3488 new,
3489 path: "sub",
3490 after: "sub/b.txt",
3491 };
3492 assert_eq!(parse_git_diff(&msg_git_diff(&diff)), Some(diff));
3493
3494 let patch = GitPatchRequest {
3495 nonce: 8,
3496 repo_id: 7,
3497 flags: GIT_PATCH_RENAMES | GIT_PATCH_CHAR_SPANS,
3498 context: 5,
3499 rename: 0,
3500 old,
3501 new,
3502 path: "a.txt",
3503 max_len: 1 << 16,
3504 after: "a.txt",
3505 after_pos: 4096,
3506 };
3507 assert_eq!(parse_git_patch(&msg_git_patch(&patch)), Some(patch));
3508
3509 let index = GitIndexRequest {
3510 nonce: 9,
3511 repo_id: 7,
3512 flags: 0,
3513 path: "sub",
3514 after: "",
3515 };
3516 assert_eq!(parse_git_index(&msg_git_index(&index)), Some(index));
3517 assert_eq!(parse_git_cancel(&msg_git_cancel(10)), Some(10));
3518
3519 let oids = vec![oid(0xAA), oid(0xBB), oid(0xCC)];
3520 let msg = msg_git_base(11, 7, &oids);
3521 assert_eq!(parse_git_base(&msg), Some((11, 7, oids)));
3522
3523 assert_eq!(
3524 parse_git_resolve(&msg_git_resolve(12, 7, "main..dev")),
3525 Some((12, 7, "main..dev"))
3526 );
3527 assert_eq!(
3528 parse_git_log_watch(&msg_git_log_watch(1, 7, GIT_LOG_FIRST_PARENT, 100, "main")),
3529 Some((1, 7, GIT_LOG_FIRST_PARENT, 100, "main"))
3530 );
3531 assert_eq!(
3533 parse_git_log_watch(&msg_git_log_watch(2, 7, 0, 0, "")),
3534 Some((2, 7, 0, 0, ""))
3535 );
3536 assert_eq!(
3537 parse_git_log_unwatch(&msg_git_log_unwatch(1, 7)),
3538 Some((1, 7))
3539 );
3540 assert_eq!(
3541 parse_git_log_ack(&msg_git_log_ack(1, 7, u32::MAX)),
3542 Some((1, 7, u32::MAX))
3543 );
3544
3545 assert_eq!(parse_git_close(&msg_git_cancel(1)), None);
3547 assert_eq!(
3549 parse_git_open(&msg_git_open(&GitOpenRequest::new(1, 0, "x"))[..5]),
3550 None
3551 );
3552 }
3553
3554 #[test]
3555 fn response_roundtrips() {
3556 let msg = msg_git_repo(
3557 1,
3558 2,
3559 GIT_STATUS_OK,
3560 GIT_OID_FORMAT_SHA1,
3561 GIT_REPO_LINKED,
3562 "/w",
3563 "/w/.git",
3564 );
3565 assert_eq!(
3566 parse_git_repo(&msg),
3567 Some(GitRepoInfo {
3568 nonce: 1,
3569 repo_id: 2,
3570 status: GIT_STATUS_OK,
3571 oid_format: GIT_OID_FORMAT_SHA1,
3572 flags: GIT_REPO_LINKED,
3573 workdir: "/w",
3574 gitdir: "/w/.git",
3575 })
3576 );
3577 let msg = msg_git_repo(
3579 1,
3580 GIT_REPO_ID_INVALID,
3581 GIT_STATUS_NOT_FOUND,
3582 0,
3583 0,
3584 "no repo",
3585 "",
3586 );
3587 let info = parse_git_repo(&msg).unwrap();
3588 assert_eq!(info.repo_id, GIT_REPO_ID_INVALID);
3589 assert_eq!(info.status, GIT_STATUS_NOT_FOUND);
3590 assert_eq!(info.workdir, "no repo");
3591 assert_eq!(info.gitdir, "");
3592
3593 let msg = msg_git_state(2, 9, GIT_STATE_REFS_TRUNCATED, b"records");
3594 assert_eq!(
3595 parse_git_state(&msg),
3596 Some((2, 9, GIT_STATE_REFS_TRUNCATED, b"records".to_vec()))
3597 );
3598
3599 assert_eq!(
3600 parse_git_closed(&msg_git_closed(2, GIT_CLOSED_REPO_GONE)),
3601 Some((2, GIT_CLOSED_REPO_GONE))
3602 );
3603
3604 let frontier = vec![oid(0xEE)];
3605 let msg = msg_git_commits(3, GIT_STATUS_OK, GIT_COMMITS_MORE, &frontier, b"recs");
3606 assert_eq!(
3607 parse_git_commits(&msg),
3608 Some(GitCommitsPage {
3609 nonce: 3,
3610 status: GIT_STATUS_OK,
3611 flags: GIT_COMMITS_MORE,
3612 frontier,
3613 records: b"recs".to_vec(),
3614 })
3615 );
3616 let msg = msg_git_commits(4, GIT_STATUS_OK, 0, &[], &[]);
3618 assert_eq!(
3619 parse_git_commits(&msg),
3620 Some(GitCommitsPage {
3621 nonce: 4,
3622 status: GIT_STATUS_OK,
3623 flags: 0,
3624 frontier: vec![],
3625 records: vec![],
3626 })
3627 );
3628
3629 let msg = msg_git_tree_resp(5, GIT_STATUS_OK, GIT_TREE_TRUNCATED, b"t");
3630 assert_eq!(
3631 parse_git_tree_resp(&msg),
3632 Some((5, GIT_STATUS_OK, GIT_TREE_TRUNCATED, b"t".to_vec()))
3633 );
3634
3635 let msg = msg_git_blob_resp(6, GIT_STATUS_OK, 11, b"hello world");
3636 assert_eq!(
3637 parse_git_blob_resp(&msg),
3638 Some((6, GIT_STATUS_OK, 11, b"hello world".to_vec()))
3639 );
3640 let msg = msg_git_blob_resp(7, GIT_STATUS_TOO_LARGE, 1 << 40, &[]);
3642 assert_eq!(
3643 parse_git_blob_resp(&msg),
3644 Some((7, GIT_STATUS_TOO_LARGE, 1 << 40, vec![]))
3645 );
3646
3647 let msg = msg_git_diff_resp(8, GIT_STATUS_OK, 0, b"d");
3648 assert_eq!(
3649 parse_git_diff_resp(&msg),
3650 Some((8, GIT_STATUS_OK, 0, b"d".to_vec()))
3651 );
3652
3653 let msg = msg_git_patch_resp(9, GIT_STATUS_OK, GIT_PATCH_STRUCTURED, b"p");
3654 assert_eq!(
3655 parse_git_patch_resp(&msg),
3656 Some((9, GIT_STATUS_OK, GIT_PATCH_STRUCTURED, b"p".to_vec()))
3657 );
3658
3659 let msg = msg_git_index_resp(10, GIT_STATUS_OK, 0, b"i");
3660 assert_eq!(
3661 parse_git_index_resp(&msg),
3662 Some((10, GIT_STATUS_OK, 0, b"i".to_vec()))
3663 );
3664
3665 let bases = vec![oid(0xAB)];
3666 let msg = msg_git_base_resp(11, GIT_STATUS_OK, &bases);
3667 assert_eq!(parse_git_base_resp(&msg), Some((11, GIT_STATUS_OK, bases)));
3668 let msg = msg_git_base_resp(12, GIT_STATUS_OK, &[]);
3670 assert_eq!(parse_git_base_resp(&msg), Some((12, GIT_STATUS_OK, vec![])));
3671
3672 let tips = vec![oid(0xCC)];
3673 let hides = vec![oid(0xDD)];
3674 let msg = msg_git_resolve_resp(13, GIT_STATUS_OK, &tips, &hides);
3675 assert_eq!(
3676 parse_git_resolve_resp(&msg),
3677 Some((13, GIT_STATUS_OK, tips, hides))
3678 );
3679 let msg = msg_git_resolve_resp(14, GIT_STATUS_OK, &[oid(0xCC)], &[]);
3681 assert_eq!(
3682 parse_git_resolve_resp(&msg),
3683 Some((14, GIT_STATUS_OK, vec![oid(0xCC)], vec![]))
3684 );
3685
3686 let frontier = vec![oid(0xEE)];
3687 let msg = msg_git_log_page(1, 42, GIT_STATUS_OK, GIT_COMMITS_MORE, &frontier, b"recs");
3688 assert_eq!(
3689 parse_git_log_page(&msg),
3690 Some(GitLogPage {
3691 log_id: 1,
3692 update_id: 42,
3693 status: GIT_STATUS_OK,
3694 flags: GIT_COMMITS_MORE,
3695 frontier,
3696 records: b"recs".to_vec(),
3697 })
3698 );
3699 let msg = msg_git_log_page(2, 0, GIT_STATUS_NOT_FOUND, 0, &[], &[]);
3701 assert_eq!(
3702 parse_git_log_page(&msg),
3703 Some(GitLogPage {
3704 log_id: 2,
3705 update_id: 0,
3706 status: GIT_STATUS_NOT_FOUND,
3707 flags: 0,
3708 frontier: vec![],
3709 records: vec![],
3710 })
3711 );
3712 }
3713
3714 #[test]
3715 fn state_record_roundtrip() {
3716 let records = vec![
3717 GitStateRecord::Head {
3718 flags: 0,
3719 oid: oid(0x01),
3720 name: "refs/heads/main",
3721 },
3722 GitStateRecord::Head {
3724 flags: GIT_HEAD_UNBORN,
3725 oid: GIT_OID_NONE,
3726 name: "refs/heads/new",
3727 },
3728 GitStateRecord::Ref {
3729 flags: GIT_REF_PEELED_VALID,
3730 oid: oid(0x02),
3731 peeled: oid(0x03),
3732 name: "refs/tags/v1",
3733 target: "",
3734 },
3735 GitStateRecord::Op {
3736 op: GIT_OP_REBASE,
3737 oid: oid(0x04),
3738 detail: "",
3739 },
3740 GitStateRecord::Status {
3741 staged: b'R',
3742 unstaged: b' ',
3743 flags: 0,
3744 oid: GIT_OID_NONE,
3745 old_path: "old.txt",
3746 path: "new.txt",
3747 },
3748 GitStateRecord::Status {
3749 staged: b'?',
3750 unstaged: b'?',
3751 flags: GIT_STATUS_ENTRY_CONFLICTED,
3752 oid: GIT_OID_NONE,
3753 old_path: "",
3754 path: "%FF.bin",
3755 },
3756 GitStateRecord::Upstream {
3757 flags: GIT_UPSTREAM_COUNTS_VALID,
3758 ahead: 2,
3759 behind: 3,
3760 name: "refs/heads/main",
3761 upstream: "refs/remotes/origin/main",
3762 },
3763 GitStateRecord::Stash {
3764 index: 0,
3765 oid: oid(0x05),
3766 time: 1_700_000_000,
3767 tz: -300,
3768 msg: "WIP on main",
3769 },
3770 ];
3771 let mut buf = Vec::new();
3772 for r in &records {
3773 append_git_state_record(&mut buf, r);
3774 }
3775 let decoded: Vec<_> = git_state_records(&buf).collect();
3776 assert_eq!(decoded, records);
3777 }
3778
3779 #[test]
3780 fn commit_record_roundtrip() {
3781 let records = vec![
3782 GitCommitRecord::Commit {
3783 flags: GIT_COMMIT_LOSSY_ENCODING,
3784 oid: oid(0x0A),
3785 tree: oid(0x0B),
3786 parents: vec![oid(0x0C), oid(0x0D)],
3787 author_time: 1_700_000_000,
3788 author_tz: 60,
3789 committer_time: 1_700_000_001,
3790 committer_tz: -300,
3791 author_name: "Ann Author",
3792 author_email: "ann@example.com",
3793 committer_name: "Cam Committer",
3794 committer_email: "cam@example.com",
3795 message: "subject\n\nbody\n",
3796 },
3797 GitCommitRecord::Commit {
3799 flags: 0,
3800 oid: oid(0x0E),
3801 tree: oid(0x0B),
3802 parents: vec![],
3803 author_time: 0,
3804 author_tz: 0,
3805 committer_time: 0,
3806 committer_tz: 0,
3807 author_name: "",
3808 author_email: "",
3809 committer_name: "",
3810 committer_email: "",
3811 message: "",
3812 },
3813 GitCommitRecord::PathAt {
3814 otype: GIT_OTYPE_BLOB,
3815 mode: 0o100644,
3816 oid: oid(0x0F),
3817 path: "src/lib.rs",
3818 },
3819 GitCommitRecord::PathAt {
3821 otype: GIT_OTYPE_BLOB,
3822 mode: 0,
3823 oid: GIT_OID_NONE,
3824 path: "gone.rs",
3825 },
3826 ];
3827 let mut buf = Vec::new();
3828 for r in &records {
3829 append_git_commit_record(&mut buf, r);
3830 }
3831 let decoded: Vec<_> = git_commit_records(&buf).collect();
3832 assert_eq!(decoded, records);
3833 }
3834
3835 #[test]
3836 fn tree_record_roundtrip() {
3837 let records = vec![
3838 GitTreeRecord::Entry {
3839 otype: GIT_OTYPE_TREE,
3840 mode: 0o40000,
3841 oid: oid(0x0E),
3842 name: "src",
3843 },
3844 GitTreeRecord::Entry {
3845 otype: GIT_OTYPE_BLOB,
3846 mode: 0o100644,
3847 oid: oid(0x0F),
3848 name: "%FF.bin", },
3850 GitTreeRecord::Entry {
3851 otype: GIT_OTYPE_COMMIT,
3852 mode: 0o160000,
3853 oid: oid(0x10),
3854 name: "submodule",
3855 },
3856 ];
3857 let mut buf = Vec::new();
3858 for r in &records {
3859 append_git_tree_record(&mut buf, r);
3860 }
3861 let decoded: Vec<_> = git_tree_records(&buf).collect();
3862 assert_eq!(decoded, records);
3863 }
3864
3865 #[test]
3866 fn diff_record_roundtrip() {
3867 let records = vec![
3868 GitDiffRecord::Base { oid: oid(0x10) },
3869 GitDiffRecord::Entry {
3870 st: b'R',
3871 similarity: 90,
3872 dflags: 0,
3873 old_mode: 0o100644,
3874 new_mode: 0o100755,
3875 old_oid: oid(0x11),
3876 new_oid: oid(0x12),
3877 old_path: "old.txt",
3878 new_path: "new.txt",
3879 },
3880 GitDiffRecord::Entry {
3882 st: b'A',
3883 similarity: 0,
3884 dflags: GIT_DIFF_ENTRY_BINARY,
3885 old_mode: 0,
3886 new_mode: 0o100644,
3887 old_oid: GIT_OID_NONE,
3888 new_oid: GIT_OID_NONE,
3889 old_path: "",
3890 new_path: "new.bin",
3891 },
3892 ];
3893 let mut buf = Vec::new();
3894 for r in &records {
3895 append_git_diff_record(&mut buf, r);
3896 }
3897 let decoded: Vec<_> = git_diff_records(&buf).collect();
3898 assert_eq!(decoded, records);
3899 }
3900
3901 #[test]
3902 fn patch_record_roundtrip() {
3903 let records = vec![
3904 GitPatchRecord::Base { oid: oid(0x13) },
3905 GitPatchRecord::File {
3906 st: b'M',
3907 similarity: 0,
3908 flags: 0,
3909 old_path: "a.txt",
3910 new_path: "a.txt",
3911 },
3912 GitPatchRecord::Row {
3913 old_line: 1,
3914 new_line: 1,
3915 old_text: b"hello world",
3916 new_text: b"hallo world",
3917 old_spans: vec![(1, 1)],
3918 new_spans: vec![(1, 1)],
3919 },
3920 GitPatchRecord::Row {
3922 old_line: 2,
3923 new_line: 2,
3924 old_text: b"same",
3925 new_text: b"same",
3926 old_spans: vec![],
3927 new_spans: vec![],
3928 },
3929 GitPatchRecord::Row {
3930 old_line: 0,
3931 new_line: 3,
3932 old_text: b"",
3933 new_text: b"added",
3934 old_spans: vec![],
3935 new_spans: vec![(0, 5)],
3936 },
3937 GitPatchRecord::Gap {
3938 old_line: 10,
3939 new_line: 11,
3940 },
3941 GitPatchRecord::File {
3942 st: b'M',
3943 similarity: 0,
3944 flags: GIT_PATCH_FILE_BINARY,
3945 old_path: "img.png",
3946 new_path: "img.png",
3947 },
3948 ];
3949 let mut buf = Vec::new();
3950 for r in &records {
3951 append_git_patch_record(&mut buf, r);
3952 }
3953 let decoded: Vec<_> = git_patch_records(&buf).collect();
3954 assert_eq!(decoded, records);
3955 }
3956
3957 #[test]
3958 fn index_record_roundtrip() {
3959 let records = vec![
3960 GitIndexRecord::Entry {
3961 stage: 0,
3962 iflags: GIT_INDEX_INTENT_TO_ADD,
3963 mode: 0o100644,
3964 size: 5,
3965 mtime_ns: 1_700_000_000_000_000_000,
3966 oid: oid(0x14),
3967 path: "a.txt",
3968 },
3969 GitIndexRecord::Entry {
3971 stage: 2,
3972 iflags: 0,
3973 mode: 0o100644,
3974 size: 0,
3975 mtime_ns: 0,
3976 oid: oid(0x15),
3977 path: "conflicted.txt",
3978 },
3979 ];
3980 let mut buf = Vec::new();
3981 for r in &records {
3982 append_git_index_record(&mut buf, r);
3983 }
3984 let decoded: Vec<_> = git_index_records(&buf).collect();
3985 assert_eq!(decoded, records);
3986 }
3987
3988 #[test]
3992 fn second_block_roundtrips() {
3993 let discover = GitDiscoverRequest {
3994 nonce: 1,
3995 flags: GIT_DISCOVER_NESTED,
3996 depth: 3,
3997 path: "/workspace",
3998 after: "/workspace/a",
3999 };
4000 assert_eq!(
4001 parse_git_discover(&msg_git_discover(&discover)),
4002 Some(discover)
4003 );
4004
4005 let blame = GitBlameRequest {
4006 nonce: 2,
4007 repo_id: 7,
4008 flags: GIT_BLAME_FOLLOW_RENAMES,
4009 oid: oid(0x21),
4010 start_line: 100,
4011 line_count: 40,
4012 path: "src/a.rs",
4013 };
4014 assert_eq!(parse_git_blame(&msg_git_blame(&blame)), Some(blame));
4015
4016 let reflog = GitReflogRequest {
4017 nonce: 3,
4018 repo_id: 7,
4019 flags: GIT_REFLOG_OLDEST_FIRST,
4020 limit: 50,
4021 ref_name: "",
4022 after_pos: 20,
4023 };
4024 assert_eq!(parse_git_reflog(&msg_git_reflog(&reflog)), Some(reflog));
4025
4026 let fetch = GitFetchRequest {
4027 nonce: 4,
4028 repo_id: 7,
4029 flags: GIT_FETCH_ANCHOR | GIT_FETCH_PRUNE,
4030 timeout_ms: 30_000,
4031 remote: "origin",
4032 refspecs: vec!["refs/pull/12/head", "deadbeef"],
4033 };
4034 assert_eq!(parse_git_fetch(&msg_git_fetch(&fetch)), Some(fetch));
4035
4036 let mut records = Vec::new();
4037 append_git_discover_record(
4038 &mut records,
4039 &GitDiscoverRecord::Repo {
4040 flags: GIT_FOUND_SUBMODULE,
4041 workdir: "/workspace/a",
4042 gitdir: "/workspace/.git/modules/a",
4043 },
4044 );
4045 append_git_discover_record(
4046 &mut records,
4047 &GitDiscoverRecord::Cursor {
4048 after: "/workspace/a",
4049 pos: 0,
4050 },
4051 );
4052 assert_eq!(git_discover_records(&records).count(), 2);
4053
4054 let mut records = Vec::new();
4055 append_git_blame_record(
4056 &mut records,
4057 &GitBlameRecord::Range {
4058 flags: 0,
4059 commit: oid(0x22),
4060 start_line: 1,
4061 line_count: 12,
4062 orig_start: 40,
4063 orig_path: "src/old.rs",
4064 },
4065 );
4066 assert_eq!(git_blame_records(&records).count(), 1);
4067
4068 let mut records = Vec::new();
4069 append_git_reflog_record(
4070 &mut records,
4071 &GitReflogRecord::Entry {
4072 flags: 0,
4073 old: oid(0x23),
4074 new: oid(0x24),
4075 time: 1_700_000_000,
4076 tz: -480,
4077 msg: "commit (amend): fix",
4078 },
4079 );
4080 assert_eq!(git_reflog_records(&records).count(), 1);
4081
4082 let mut records = Vec::new();
4083 append_git_fetch_record(
4084 &mut records,
4085 &GitFetchRecord::Ref {
4086 flags: GIT_FETCH_REF_NEW,
4087 status: GIT_STATUS_OK,
4088 old: GIT_OID_NONE,
4089 new: oid(0x25),
4090 name: "refs/blit/fetch/origin/0",
4091 detail: "",
4092 },
4093 );
4094 assert_eq!(git_fetch_records(&records).count(), 1);
4095
4096 let mut buf = Vec::new();
4099 append_git_tree_record(&mut buf, &GitTreeRecord::Cursor { after: "z", pos: 0 });
4100 assert_eq!(
4101 git_tree_records(&buf).next(),
4102 Some(GitTreeRecord::Cursor { after: "z", pos: 0 })
4103 );
4104 let mut buf = Vec::new();
4105 append_git_diff_record(&mut buf, &GitDiffRecord::Cursor { after: "z", pos: 0 });
4106 assert_eq!(
4107 git_diff_records(&buf).next(),
4108 Some(GitDiffRecord::Cursor { after: "z", pos: 0 })
4109 );
4110 let mut buf = Vec::new();
4111 append_git_patch_record(
4112 &mut buf,
4113 &GitPatchRecord::Cursor {
4114 after: "z",
4115 pos: 99,
4116 },
4117 );
4118 assert_eq!(
4119 git_patch_records(&buf).next(),
4120 Some(GitPatchRecord::Cursor {
4121 after: "z",
4122 pos: 99
4123 })
4124 );
4125 let mut buf = Vec::new();
4126 append_git_index_record(&mut buf, &GitIndexRecord::Cursor { after: "z", pos: 0 });
4127 assert_eq!(
4128 git_index_records(&buf).next(),
4129 Some(GitIndexRecord::Cursor { after: "z", pos: 0 })
4130 );
4131 }
4132
4133 #[test]
4137 fn partial_state_chunks_install_together() {
4138 let mut first = Vec::new();
4139 append_git_state_record(
4140 &mut first,
4141 &GitStateRecord::Head {
4142 flags: 0,
4143 oid: oid(1),
4144 name: "refs/heads/main",
4145 },
4146 );
4147 let mut second = Vec::new();
4148 append_git_state_record(
4149 &mut second,
4150 &GitStateRecord::Ref {
4151 flags: GIT_REF_SYMBOLIC,
4152 oid: oid(2),
4153 peeled: GIT_OID_NONE,
4154 name: "refs/remotes/origin/HEAD",
4155 target: "refs/remotes/origin/trunk",
4156 },
4157 );
4158
4159 let mut mirror = GitStateMirror::new();
4160 assert_eq!(
4161 mirror.apply_state(&msg_git_state(1, 5, GIT_STATE_PARTIAL, &first)),
4162 GitStateApply::Partial
4163 );
4164 assert!(mirror.head.is_none());
4166 assert_eq!(
4167 mirror.apply_state(&msg_git_state(1, 5, 0, &second)),
4168 GitStateApply::Complete(5)
4169 );
4170 assert_eq!(mirror.head.as_ref().unwrap().name, "refs/heads/main");
4171 assert_eq!(
4172 mirror.refs["refs/remotes/origin/HEAD"].target,
4173 "refs/remotes/origin/trunk"
4174 );
4175 }
4176
4177 #[test]
4178 fn unknown_record_kind_is_skipped() {
4179 let mut unknown = Vec::new();
4183 unknown.extend_from_slice(&4u32.to_le_bytes());
4184 unknown.push(0x6E);
4185 unknown.extend_from_slice(&[1, 2, 3]);
4186
4187 let mut buf = unknown.clone();
4188 append_git_state_record(
4189 &mut buf,
4190 &GitStateRecord::Op {
4191 op: GIT_OP_MERGE,
4192 oid: oid(1),
4193 detail: "",
4194 },
4195 );
4196 assert_eq!(git_state_records(&buf).count(), 1);
4197
4198 let mut buf = unknown.clone();
4199 append_git_commit_record(
4200 &mut buf,
4201 &GitCommitRecord::PathAt {
4202 otype: GIT_OTYPE_BLOB,
4203 mode: 0,
4204 oid: oid(1),
4205 path: "p",
4206 },
4207 );
4208 assert_eq!(git_commit_records(&buf).count(), 1);
4209
4210 let mut buf = unknown.clone();
4211 append_git_tree_record(
4212 &mut buf,
4213 &GitTreeRecord::Entry {
4214 otype: GIT_OTYPE_BLOB,
4215 mode: 0,
4216 oid: oid(1),
4217 name: "n",
4218 },
4219 );
4220 assert_eq!(git_tree_records(&buf).count(), 1);
4221
4222 let mut buf = unknown.clone();
4223 append_git_diff_record(&mut buf, &GitDiffRecord::Base { oid: oid(1) });
4224 assert_eq!(git_diff_records(&buf).count(), 1);
4225
4226 let mut buf = unknown.clone();
4227 append_git_patch_record(
4228 &mut buf,
4229 &GitPatchRecord::Gap {
4230 old_line: 1,
4231 new_line: 1,
4232 },
4233 );
4234 assert_eq!(git_patch_records(&buf).count(), 1);
4235
4236 let mut buf = unknown.clone();
4237 append_git_index_record(
4238 &mut buf,
4239 &GitIndexRecord::Entry {
4240 stage: 0,
4241 iflags: 0,
4242 mode: 0,
4243 size: 0,
4244 mtime_ns: 0,
4245 oid: oid(1),
4246 path: "p",
4247 },
4248 );
4249 assert_eq!(git_index_records(&buf).count(), 1);
4250 }
4251
4252 #[test]
4253 fn malformed_record_ends_iteration() {
4254 let mut buf = Vec::new();
4256 buf.extend_from_slice(&2u32.to_le_bytes());
4257 buf.push(GIT_STATE_RECORD_HEAD);
4258 buf.push(0);
4259 append_git_state_record(
4260 &mut buf,
4261 &GitStateRecord::Head {
4262 flags: 0,
4263 oid: oid(1),
4264 name: "refs/heads/main",
4265 },
4266 );
4267 assert_eq!(git_state_records(&buf).next(), None);
4268 }
4269
4270 #[test]
4271 fn oversized_declared_length_is_rejected_before_allocation() {
4272 fn forged(opcode: u8, header: &[u8]) -> Vec<u8> {
4274 let mut msg = vec![opcode];
4275 msg.extend_from_slice(header);
4276 msg.extend_from_slice(&(1u32 << 30).to_le_bytes());
4277 msg.extend_from_slice(&[0u8; 16]);
4278 msg
4279 }
4280
4281 let state = forged(S2C_GIT_STATE, &[1, 0, 1, 0, 0, 0, 0]);
4282 assert_eq!(parse_git_state(&state), None);
4283 assert_eq!(
4284 GitStateMirror::new().apply_state(&state),
4285 GitStateApply::Malformed
4286 );
4287 assert_eq!(
4288 parse_git_commits(&forged(S2C_GIT_COMMITS, &[1, 0, 0, 0, 0, 0])),
4289 None
4290 );
4291 assert_eq!(
4292 parse_git_tree_resp(&forged(S2C_GIT_TREE, &[1, 0, 0, 0])),
4293 None
4294 );
4295 assert_eq!(
4296 parse_git_blob_resp(&forged(S2C_GIT_BLOB, &[1, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0])),
4297 None
4298 );
4299 assert_eq!(
4300 parse_git_diff_resp(&forged(S2C_GIT_DIFF, &[1, 0, 0, 0])),
4301 None
4302 );
4303 assert_eq!(
4304 parse_git_patch_resp(&forged(S2C_GIT_PATCH, &[1, 0, 0, 0])),
4305 None
4306 );
4307 assert_eq!(
4308 parse_git_index_resp(&forged(S2C_GIT_INDEX, &[1, 0, 0, 0])),
4309 None
4310 );
4311 }
4312
4313 #[test]
4314 fn state_mirror_replaces_whole_state() {
4315 let mut mirror = GitStateMirror::new();
4316
4317 let mut records = Vec::new();
4318 append_git_state_record(
4319 &mut records,
4320 &GitStateRecord::Head {
4321 flags: 0,
4322 oid: oid(1),
4323 name: "refs/heads/main",
4324 },
4325 );
4326 append_git_state_record(
4327 &mut records,
4328 &GitStateRecord::Ref {
4329 flags: 0,
4330 oid: oid(1),
4331 peeled: GIT_OID_NONE,
4332 name: "refs/heads/main",
4333 target: "",
4334 },
4335 );
4336 append_git_state_record(
4337 &mut records,
4338 &GitStateRecord::Ref {
4339 flags: GIT_REF_PEELED_VALID,
4340 oid: oid(2),
4341 peeled: oid(3),
4342 name: "refs/tags/v1",
4343 target: "",
4344 },
4345 );
4346 append_git_state_record(
4347 &mut records,
4348 &GitStateRecord::Op {
4349 op: GIT_OP_MERGE,
4350 oid: oid(4),
4351 detail: "",
4352 },
4353 );
4354 append_git_state_record(
4355 &mut records,
4356 &GitStateRecord::Status {
4357 staged: b'M',
4358 unstaged: b' ',
4359 flags: 0,
4360 oid: GIT_OID_NONE,
4361 old_path: "",
4362 path: "a.txt",
4363 },
4364 );
4365 append_git_state_record(
4366 &mut records,
4367 &GitStateRecord::Upstream {
4368 flags: GIT_UPSTREAM_COUNTS_VALID,
4369 ahead: 2,
4370 behind: 3,
4371 name: "refs/heads/main",
4372 upstream: "refs/remotes/origin/main",
4373 },
4374 );
4375 append_git_state_record(
4376 &mut records,
4377 &GitStateRecord::Stash {
4378 index: 0,
4379 oid: oid(5),
4380 time: 1_700_000_000,
4381 tz: -300,
4382 msg: "WIP on main",
4383 },
4384 );
4385 let msg = msg_git_state(1, 1, GIT_STATE_STATUS_TRUNCATED, &records);
4386 assert_eq!(mirror.apply_state(&msg), GitStateApply::Complete(1));
4387 assert_eq!(
4388 mirror.head,
4389 Some(GitHead {
4390 flags: 0,
4391 oid: oid(1),
4392 name: "refs/heads/main".to_string(),
4393 })
4394 );
4395 assert_eq!(mirror.refs.len(), 2);
4396 assert_eq!(mirror.refs["refs/tags/v1"].peeled, oid(3));
4397 assert_eq!(mirror.op.as_ref().unwrap().op, GIT_OP_MERGE);
4398 assert_eq!(mirror.status.len(), 1);
4399 assert_eq!(mirror.upstreams["refs/heads/main"].ahead, 2);
4400 assert_eq!(mirror.stashes[0].message, "WIP on main");
4401 assert_eq!(mirror.flags, GIT_STATE_STATUS_TRUNCATED);
4402
4403 let mut records = Vec::new();
4406 append_git_state_record(
4407 &mut records,
4408 &GitStateRecord::Head {
4409 flags: GIT_HEAD_DETACHED,
4410 oid: oid(6),
4411 name: "",
4412 },
4413 );
4414 let msg = msg_git_state(1, 2, 0, &records);
4415 assert_eq!(mirror.apply_state(&msg), GitStateApply::Complete(2));
4416 assert_eq!(mirror.head.as_ref().unwrap().flags, GIT_HEAD_DETACHED);
4417 assert!(mirror.refs.is_empty());
4418 assert_eq!(mirror.op, None);
4419 assert!(mirror.status.is_empty());
4420 assert!(mirror.upstreams.is_empty());
4421 assert!(mirror.stashes.is_empty());
4422 assert_eq!(mirror.flags, 0);
4423
4424 assert_eq!(
4426 mirror.apply_state(&msg_git_closed(1, 0)),
4427 GitStateApply::Malformed
4428 );
4429 assert_eq!(mirror.apply_state(&msg[..6]), GitStateApply::Malformed);
4430 }
4431
4432 #[test]
4438 fn wire_fixtures() {
4439 let zeros = "0".repeat(24);
4440 let zero_oid = "0".repeat(64);
4441 let o = |fill: u8| format!("{}{zeros}", hex(&[fill; 20]));
4442
4443 assert_eq!(
4444 hex(&msg_git_open(&GitOpenRequest {
4445 refs_latency_ms: 50,
4446 status_latency_ms: 500,
4447 ..GitOpenRequest::new(0x0102, GIT_OPEN_WATCH | GIT_OPEN_STATUS, "/repo")
4448 })),
4449 "a0020103003200f401ffffffff000005002f7265706f"
4450 );
4451 assert_eq!(
4453 hex(&msg_git_open(&GitOpenRequest {
4454 parent_repo_id: 4,
4455 ref_prefixes: vec!["refs/heads/"],
4456 ..GitOpenRequest::new(1, GIT_OPEN_WATCH, "vendor/lib")
4457 })),
4458 "a00100010000000000ffff04000100\
4459 0b00726566732f68656164732f0a0076656e646f722f6c6962"
4460 );
4461 assert_eq!(hex(&msg_git_close(7)), "a10700");
4462 assert_eq!(hex(&msg_git_ack(7, 0x01020304)), "a2070004030201");
4463 assert_eq!(
4464 hex(&msg_git_log(
4465 3,
4466 7,
4467 GIT_LOG_FIRST_PARENT,
4468 100,
4469 "src",
4470 &[oid(0xAA)],
4471 &[oid(0xBB)]
4472 )),
4473 format!("a70300070001640003007372630100{}0100{}", o(0xAA), o(0xBB))
4474 );
4475 assert_eq!(
4476 hex(&msg_git_tree(&GitTreeRequest {
4477 nonce: 4,
4478 repo_id: 7,
4479 flags: 0,
4480 oid: oid(0xCC),
4481 path: "dir/%FF",
4482 after: "",
4483 })),
4484 format!("ab0400070000{}07006469722f2546460000", o(0xCC))
4485 );
4486 assert_eq!(
4487 hex(&msg_git_blob(&GitBlobRequest {
4488 nonce: 5,
4489 repo_id: 7,
4490 flags: 0,
4491 oid: oid(0xDD),
4492 path: "",
4493 offset: 0,
4494 max_len: 1 << 20,
4495 })),
4496 format!("ac0500070000{}0000000000000000000000001000", o(0xDD))
4497 );
4498 let old = GitEndpoint {
4499 kind: GIT_ENDPOINT_COMMIT,
4500 oid: oid(0x11),
4501 };
4502 let new = GitEndpoint {
4503 kind: GIT_ENDPOINT_WORKTREE,
4504 oid: GIT_OID_NONE,
4505 };
4506 assert_eq!(
4507 hex(&msg_git_diff(&GitDiffRequest {
4508 nonce: 6,
4509 repo_id: 7,
4510 flags: GIT_DIFF_RENAMES,
4511 rename: 50,
4512 old,
4513 new,
4514 path: "",
4515 after: "",
4516 })),
4517 format!("ad06000700013201{}04{zero_oid}00000000", o(0x11))
4518 );
4519 assert_eq!(
4520 hex(&msg_git_patch(&GitPatchRequest {
4521 nonce: 8,
4522 repo_id: 7,
4523 flags: GIT_PATCH_RENAMES | GIT_PATCH_CHAR_SPANS,
4524 context: 5,
4525 rename: 0,
4526 old,
4527 new,
4528 path: "a.txt",
4529 max_len: 0,
4530 after: "",
4531 after_pos: 0,
4532 })),
4533 format!(
4534 "ae080007008100050001{}04{zero_oid}\
4535 0500612e7478740000000000000000000000000000",
4536 o(0x11)
4537 )
4538 );
4539 assert_eq!(
4540 hex(&msg_git_index(&GitIndexRequest {
4541 nonce: 9,
4542 repo_id: 7,
4543 flags: 0,
4544 path: "sub",
4545 after: "",
4546 })),
4547 "af090007000003007375620000"
4548 );
4549 assert_eq!(hex(&msg_git_cancel(10)), "a30a00");
4550 assert_eq!(
4551 hex(&msg_git_base(11, 7, &[oid(0xAA), oid(0xBB)])),
4552 format!("b00b00070002{}{}", o(0xAA), o(0xBB))
4553 );
4554 assert_eq!(
4555 hex(&msg_git_resolve(12, 7, "main..dev")),
4556 "a60c00070009006d61696e2e2e646576"
4557 );
4558 assert_eq!(
4559 hex(&msg_git_resolve_resp(
4560 12,
4561 GIT_STATUS_OK,
4562 &[oid(0xCC)],
4563 &[oid(0xDD)]
4564 )),
4565 format!("a60c00000100{}0100{}", o(0xCC), o(0xDD))
4566 );
4567 assert_eq!(
4568 hex(&msg_git_log_watch(1, 7, GIT_LOG_FIRST_PARENT, 100, "main")),
4569 "a80100070001640004006d61696e"
4570 );
4571 assert_eq!(hex(&msg_git_log_unwatch(1, 7)), "a901000700");
4572 assert_eq!(
4573 hex(&msg_git_log_ack(1, 7, 0x0102_0304)),
4574 "aa0100070004030201"
4575 );
4576 assert_eq!(
4577 hex(&msg_git_repo(
4578 0x0102,
4579 1,
4580 GIT_STATUS_OK,
4581 GIT_OID_FORMAT_SHA1,
4582 GIT_REPO_LINKED,
4583 "/w",
4584 "/w/.git"
4585 )),
4586 "a00201010000000802002f7707002f772f2e676974"
4587 );
4588 assert_eq!(hex(&msg_git_closed(1, GIT_CLOSED_REPO_GONE)), "a5010001");
4589 assert_eq!(
4590 hex(&msg_git_base_resp(11, GIT_STATUS_OK, &[oid(0xAB)])),
4591 format!("b00b000001{}", o(0xAB))
4592 );
4593
4594 let mut state = Vec::new();
4596 append_git_state_record(
4597 &mut state,
4598 &GitStateRecord::Head {
4599 flags: 0,
4600 oid: oid(0x01),
4601 name: "refs/heads/main",
4602 },
4603 );
4604 append_git_state_record(
4605 &mut state,
4606 &GitStateRecord::Upstream {
4607 flags: GIT_UPSTREAM_COUNTS_VALID,
4608 ahead: 2,
4609 behind: 3,
4610 name: "refs/heads/main",
4611 upstream: "refs/remotes/origin/main",
4612 },
4613 );
4614 assert_eq!(
4615 hex(&state),
4616 "33000000010001010101010101010101010101010101010101010000000000000000000000000f00726566732f68656164732f6d61696e35000000050202000000030000000f00726566732f68656164732f6d61696e1800726566732f72656d6f7465732f6f726967696e2f6d61696e"
4617 );
4618
4619 let mut commits = Vec::new();
4620 append_git_commit_record(
4621 &mut commits,
4622 &GitCommitRecord::Commit {
4623 flags: GIT_COMMIT_LOSSY_ENCODING,
4624 oid: oid(0x0A),
4625 tree: oid(0x0B),
4626 parents: vec![oid(0x0C)],
4627 author_time: 1_700_000_000,
4628 author_tz: 60,
4629 committer_time: 1_700_000_001,
4630 committer_tz: -300,
4631 author_name: "Ann Author",
4632 author_email: "ann@example.com",
4633 committer_name: "Cam Committer",
4634 committer_email: "cam@example.com",
4635 message: "subject",
4636 },
4637 );
4638 append_git_commit_record(
4639 &mut commits,
4640 &GitCommitRecord::PathAt {
4641 otype: GIT_OTYPE_BLOB,
4642 mode: 0o100644,
4643 oid: oid(0x0D),
4644 path: "src/lib.rs",
4645 },
4646 );
4647 assert_eq!(
4648 hex(&commits),
4649 "bf00000001010a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0000000000000000000000000b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b000000000000000000000000010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c00000000000000000000000000f15365000000003c0001f1536500000000d4fe0a00416e6e20417574686f720f00616e6e406578616d706c652e636f6d0d0043616d20436f6d6d69747465720f0063616d406578616d706c652e636f6d070000007375626a656374320000000203a48100000d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0000000000000000000000000a007372632f6c69622e7273"
4650 );
4651
4652 let mut tree = Vec::new();
4653 append_git_tree_record(
4654 &mut tree,
4655 &GitTreeRecord::Entry {
4656 otype: GIT_OTYPE_TREE,
4657 mode: 0o40000,
4658 oid: oid(0x0E),
4659 name: "src",
4660 },
4661 );
4662 append_git_tree_record(
4663 &mut tree,
4664 &GitTreeRecord::Entry {
4665 otype: GIT_OTYPE_BLOB,
4666 mode: 0o100644,
4667 oid: oid(0x0F),
4668 name: "%FF.bin",
4669 },
4670 );
4671 assert_eq!(
4672 hex(&tree),
4673 "2b0000000202004000000e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e00000000000000000000000003007372632f0000000203a48100000f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f00000000000000000000000007002546462e62696e"
4674 );
4675
4676 let mut diff = Vec::new();
4677 append_git_diff_record(&mut diff, &GitDiffRecord::Base { oid: oid(0x10) });
4678 append_git_diff_record(
4679 &mut diff,
4680 &GitDiffRecord::Entry {
4681 st: b'R',
4682 similarity: 90,
4683 dflags: 0,
4684 old_mode: 0o100644,
4685 new_mode: 0o100644,
4686 old_oid: oid(0x11),
4687 new_oid: oid(0x12),
4688 old_path: "old.txt",
4689 new_path: "new.txt",
4690 },
4691 );
4692 assert_eq!(
4693 hex(&diff),
4694 "210000000410101010101010101010101010101010101010100000000000000000000000005e00000003525a00a4810000a48100001111111111111111111111111111111111111111000000000000000000000000121212121212121212121212121212121212121200000000000000000000000007006f6c642e74787407006e65772e747874"
4695 );
4696
4697 let mut patch = Vec::new();
4698 append_git_patch_record(
4699 &mut patch,
4700 &GitPatchRecord::File {
4701 st: b'M',
4702 similarity: 0,
4703 flags: 0,
4704 old_path: "a.txt",
4705 new_path: "a.txt",
4706 },
4707 );
4708 append_git_patch_record(
4709 &mut patch,
4710 &GitPatchRecord::Row {
4711 old_line: 1,
4712 new_line: 1,
4713 old_text: b"hello",
4714 new_text: b"hallo",
4715 old_spans: vec![(1, 1)],
4716 new_spans: vec![(1, 1)],
4717 },
4718 );
4719 append_git_patch_record(
4720 &mut patch,
4721 &GitPatchRecord::Gap {
4722 old_line: 3,
4723 new_line: 3,
4724 },
4725 );
4726 assert_eq!(
4727 hex(&patch),
4728 "12000000014d00000500612e7478740500612e7478742f0000000201000000010000000500000068656c6c6f0500000068616c6c6f010001000000010000000100010000000100000009000000030300000003000000"
4729 );
4730
4731 let mut index = Vec::new();
4732 append_git_index_record(
4733 &mut index,
4734 &GitIndexRecord::Entry {
4735 stage: 0,
4736 iflags: GIT_INDEX_INTENT_TO_ADD,
4737 mode: 0o100644,
4738 size: 5,
4739 mtime_ns: 1_700_000_000_000_000_000,
4740 oid: oid(0x14),
4741 path: "a.txt",
4742 },
4743 );
4744 assert_eq!(
4745 hex(&index),
4746 "3e000000040001a4810000050000000000000000002a36fe9c971714141414141414141414141414141414141414140000000000000000000000000500612e747874"
4747 );
4748
4749 assert_eq!(git_state_records(&state).count(), 2);
4751 assert_eq!(git_commit_records(&commits).count(), 2);
4752 assert_eq!(git_tree_records(&tree).count(), 2);
4753 assert_eq!(git_diff_records(&diff).count(), 2);
4754 assert_eq!(git_patch_records(&patch).count(), 3);
4755 assert_eq!(git_index_records(&index).count(), 1);
4756 }
4757}