1use crate::types::{WireVersion, decode_path};
3use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
4use buntrail_core::contracts as c;
5use serde::{Deserialize, Serialize};
6
7pub fn invalid() -> c::CoreError {
8 c::CoreError::new(c::ErrorCode::InvalidArgument, "Invalid service wire value")
9}
10pub fn path(p: &c::TargetPath) -> Result<String, c::CoreError> {
11 match p {
12 c::TargetPath::PosixBytes(b) => {
13 c::TargetPath::posix(b.clone())?;
14 Ok(B64.encode(b))
15 }
16 _ => Err(invalid()),
17 }
18}
19pub fn target_path(p: &str) -> Result<c::TargetPath, c::CoreError> {
20 c::TargetPath::posix(decode_path(p).map_err(|_| invalid())?)
21}
22pub fn uuid(b: [u8; 16]) -> String {
23 let s: String = b.iter().map(|b| format!("{b:02x}")).collect();
24 format!(
25 "{}-{}-{}-{}-{}",
26 &s[..8],
27 &s[8..12],
28 &s[12..16],
29 &s[16..20],
30 &s[20..]
31 )
32}
33pub fn id(s: &str) -> Result<[u8; 16], c::CoreError> {
34 if !crate::types::uuid(s) {
35 return Err(invalid());
36 }
37 let s = s.replace('-', "");
38 let mut b = [0; 16];
39 for (i, v) in b.iter_mut().enumerate() {
40 *v = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).map_err(|_| invalid())?;
41 }
42 Ok(b)
43}
44pub fn hex(b: &[u8]) -> String {
45 b.iter().map(|b| format!("{b:02x}")).collect()
46}
47pub fn digest(s: &str) -> Result<[u8; 32], c::CoreError> {
48 if s.len() != 64 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
49 return Err(invalid());
50 }
51 let mut b = [0; 32];
52 for (i, v) in b.iter_mut().enumerate() {
53 *v = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).map_err(|_| invalid())?;
54 }
55 Ok(b)
56}
57pub fn version(v: &c::FileVersion) -> WireVersion {
58 WireVersion {
59 opaque_token: B64.encode(&v.opaque_token),
60 content_digest: hex(&v.content_digest),
61 file_identity: B64.encode(&v.file_identity),
62 }
63}
64impl WireVersion {
65 pub fn core(&self, t: &c::TargetIdentity) -> Result<c::FileVersion, c::CoreError> {
66 Ok(c::FileVersion {
67 target: t.clone(),
68 opaque_token: B64.decode(&self.opaque_token).map_err(|_| invalid())?,
69 content_digest: digest(&self.content_digest)?,
70 file_identity: B64.decode(&self.file_identity).map_err(|_| invalid())?,
71 })
72 }
73}
74#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct File {
77 pub path_base64: String,
78 #[serde(deserialize_with = "crate::types::required_option")]
79 pub resolved_identity: Option<String>,
80}
81impl File {
82 pub fn from_core(f: &c::FileRef) -> Result<Self, c::CoreError> {
83 Ok(Self {
84 path_base64: path(&f.path)?,
85 resolved_identity: f.resolved_identity.as_ref().map(|b| B64.encode(b)),
86 })
87 }
88 pub fn core(&self, t: &c::TargetIdentity) -> Result<c::FileRef, c::CoreError> {
89 Ok(c::FileRef {
90 target: t.clone(),
91 path: target_path(&self.path_base64)?,
92 resolved_identity: self
93 .resolved_identity
94 .as_ref()
95 .map(|s| B64.decode(s).map_err(|_| invalid()))
96 .transpose()?,
97 })
98 }
99}
100#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(deny_unknown_fields)]
102pub struct Worktree {
103 pub path_base64: String,
104 pub common_directory_base64: String,
105}
106impl Worktree {
107 pub fn from_core(w: &c::WorktreeIdentity) -> Result<Self, c::CoreError> {
108 Ok(Self {
109 path_base64: path(&w.canonical_path)?,
110 common_directory_base64: B64.encode(&w.common_git_directory_identity),
111 })
112 }
113 pub fn core(&self, t: &c::TargetIdentity) -> Result<c::WorktreeIdentity, c::CoreError> {
114 Ok(c::WorktreeIdentity {
115 target: t.clone(),
116 canonical_path: target_path(&self.path_base64)?,
117 common_git_directory_identity: B64
118 .decode(&self.common_directory_base64)
119 .map_err(|_| invalid())?,
120 })
121 }
122}
123#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
125pub enum Head {
126 Branch { name_base64: String, oid: String },
127 Detached { oid: String },
128 Unborn { name_base64: String },
129}
130impl Head {
131 fn from_core(h: &c::GitHead) -> Self {
132 match h {
133 c::GitHead::Branch { name, oid } => Self::Branch {
134 name_base64: B64.encode(name),
135 oid: oid.clone(),
136 },
137 c::GitHead::Detached(oid) => Self::Detached { oid: oid.clone() },
138 c::GitHead::Unborn(name) => Self::Unborn {
139 name_base64: B64.encode(name),
140 },
141 }
142 }
143 fn core(&self) -> Result<c::GitHead, c::CoreError> {
144 Ok(match self {
145 Self::Branch { name_base64, oid } => c::GitHead::Branch {
146 name: B64.decode(name_base64).map_err(|_| invalid())?,
147 oid: oid.clone(),
148 },
149 Self::Detached { oid } => c::GitHead::Detached(oid.clone()),
150 Self::Unborn { name_base64 } => {
151 c::GitHead::Unborn(B64.decode(name_base64).map_err(|_| invalid())?)
152 }
153 })
154 }
155}
156#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(deny_unknown_fields)]
158pub struct GitState {
159 pub head: Head,
160 pub index_digest: String,
161 pub worktree_digest: String,
162}
163impl GitState {
164 pub fn from_core(s: &c::GitExpectedState) -> Self {
165 Self {
166 head: Head::from_core(&s.head),
167 index_digest: hex(&s.index_fingerprint),
168 worktree_digest: hex(&s.worktree_fingerprint),
169 }
170 }
171 pub fn core(&self) -> Result<c::GitExpectedState, c::CoreError> {
172 Ok(c::GitExpectedState {
173 head: self.head.core()?,
174 index_fingerprint: digest(&self.index_digest)?,
175 worktree_fingerprint: digest(&self.worktree_digest)?,
176 })
177 }
178}
179#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
181pub enum GitCommand {
182 Repository { action: c::RepositoryAction },
183 Stage { paths: Vec<String> },
184 Unstage { paths: Vec<String> },
185 Commit { message: String },
186 Checkout { reference: String },
187 Fetch { remote: String },
188 Push { remote: String, reference: String },
189 PullFastForward { remote: String, reference: String },
190}
191impl GitCommand {
192 pub fn from_core(c: &c::GitMutation) -> Result<Self, c::CoreError> {
193 Ok(match c {
194 c::GitMutation::Repository(action) => Self::Repository {
195 action: action.clone(),
196 },
197 c::GitMutation::Stage(p) => Self::Stage {
198 paths: p.iter().map(path).collect::<Result<_, _>>()?,
199 },
200 c::GitMutation::Unstage(p) => Self::Unstage {
201 paths: p.iter().map(path).collect::<Result<_, _>>()?,
202 },
203 c::GitMutation::Commit { message } => Self::Commit {
204 message: message.clone(),
205 },
206 c::GitMutation::Checkout { reference } => Self::Checkout {
207 reference: B64.encode(reference),
208 },
209 c::GitMutation::Fetch { remote } => Self::Fetch {
210 remote: B64.encode(remote),
211 },
212 c::GitMutation::Push { remote, reference } => Self::Push {
213 remote: B64.encode(remote),
214 reference: B64.encode(reference),
215 },
216 c::GitMutation::PullFastForward { remote, reference } => Self::PullFastForward {
217 remote: B64.encode(remote),
218 reference: B64.encode(reference),
219 },
220 })
221 }
222 pub fn core(&self) -> Result<c::GitMutation, c::CoreError> {
223 let b = |s: &str| B64.decode(s).map_err(|_| invalid());
224 Ok(match self {
225 Self::Repository { action } => c::GitMutation::Repository(action.clone()),
226 Self::Stage { paths } => c::GitMutation::Stage(
227 paths
228 .iter()
229 .map(|p| target_path(p))
230 .collect::<Result<_, _>>()?,
231 ),
232 Self::Unstage { paths } => c::GitMutation::Unstage(
233 paths
234 .iter()
235 .map(|p| target_path(p))
236 .collect::<Result<_, _>>()?,
237 ),
238 Self::Commit { message } => c::GitMutation::Commit {
239 message: message.clone(),
240 },
241 Self::Checkout { reference } => c::GitMutation::Checkout {
242 reference: b(reference)?,
243 },
244 Self::Fetch { remote } => c::GitMutation::Fetch { remote: b(remote)? },
245 Self::Push { remote, reference } => c::GitMutation::Push {
246 remote: b(remote)?,
247 reference: b(reference)?,
248 },
249 Self::PullFastForward { remote, reference } => c::GitMutation::PullFastForward {
250 remote: b(remote)?,
251 reference: b(reference)?,
252 },
253 })
254 }
255}
256#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
258pub enum FileCommand {
259 CreateDirectory,
260 Rename {
261 destination: String,
262 },
263 Move {
264 destination: String,
265 },
266 Remove {
267 confirmed_target: String,
268 confirmed_path: String,
269 },
270 Delete {
271 confirmed_target: String,
272 confirmed_path: String,
273 },
274}
275#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(deny_unknown_fields)]
277pub struct Search {
278 pub root: File,
279 pub expression: String,
280 pub regex: bool,
281 pub case_sensitive: bool,
282 pub whole_word: bool,
283 pub includes: Vec<String>,
284 pub excludes: Vec<String>,
285 pub hidden: bool,
286 pub ignored: bool,
287}
288impl Search {
289 pub fn from_core(r: &c::SearchRequest) -> Result<Self, c::CoreError> {
290 Ok(Self {
291 root: File::from_core(&r.root)?,
292 expression: r.expression.clone(),
293 regex: matches!(r.mode, c::SearchMode::Regex),
294 case_sensitive: r.case_sensitive,
295 whole_word: r.whole_word,
296 includes: r.includes.clone(),
297 excludes: r.excludes.clone(),
298 hidden: r.hidden,
299 ignored: r.ignored,
300 })
301 }
302 pub fn core(&self, t: &c::TargetIdentity, id: u64) -> Result<c::SearchRequest, c::CoreError> {
303 Ok(c::SearchRequest {
304 request: c::RequestId(id),
305 root: self.root.core(t)?,
306 expression: self.expression.clone(),
307 mode: if self.regex {
308 c::SearchMode::Regex
309 } else {
310 c::SearchMode::Literal
311 },
312 case_sensitive: self.case_sensitive,
313 whole_word: self.whole_word,
314 includes: self.includes.clone(),
315 excludes: self.excludes.clone(),
316 hidden: self.hidden,
317 ignored: self.ignored,
318 })
319 }
320}
321#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(deny_unknown_fields)]
323pub struct LeaseOwner {
324 pub client_id: String,
325 pub workspace_id: String,
326 pub lease_id: String,
327 pub target_binding: String,
328 pub runtime_generation: String,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub container_start_base64: Option<String>,
331}
332impl LeaseOwner {
333 pub fn from_core(
334 owner: &c::WorkspaceLease,
335 target: &c::TargetIdentity,
336 ) -> Result<Self, c::CoreError> {
337 use sha2::Digest;
338 if owner
339 .runtime
340 .container_start
341 .as_ref()
342 .is_some_and(|s| s.is_empty() || s.len() > 256)
343 {
344 return Err(invalid());
345 }
346 Ok(Self {
347 client_id: uuid(owner.client.0),
348 workspace_id: uuid(owner.workspace.0),
349 lease_id: uuid(owner.lease.0),
350 target_binding: hex(&sha2::Sha256::digest(
351 serde_json::to_vec(target).map_err(|_| invalid())?,
352 )),
353 runtime_generation: uuid(owner.runtime.supervisor_boot),
354 container_start_base64: owner
355 .runtime
356 .container_start
357 .as_ref()
358 .map(|s| B64.encode(s)),
359 })
360 }
361 pub fn context(&self) -> crate::types::Context {
362 crate::types::Context {
363 client_id: self.client_id.clone(),
364 workspace_id: self.workspace_id.clone(),
365 target_binding: self.target_binding.clone(),
366 runtime_generation: self.runtime_generation.clone(),
367 }
368 }
369 pub fn validate(&self) -> Result<(), c::CoreError> {
370 id(&self.client_id)?;
371 id(&self.workspace_id)?;
372 id(&self.lease_id)?;
373 id(&self.runtime_generation)?;
374 digest(&self.target_binding)?;
375 self.container_start()?;
376 Ok(())
377 }
378 pub fn container_start(&self) -> Result<Option<Vec<u8>>, c::CoreError> {
379 crate::types::decode_container_start(self.container_start_base64.as_deref())
380 .map_err(|_| invalid())
381 }
382}
383#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
385pub enum OwnerPresence {
386 Active {},
387 Detached { remaining_millis: u64 },
388 Absent {},
389 RuntimeLost {},
390}
391#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
392#[serde(tag = "method", rename_all = "snake_case", deny_unknown_fields)]
393pub enum Call {
394 Repository {
395 worktree: Worktree,
396 },
397 Commit {
398 worktree: Worktree,
399 oid: String,
400 cursor: Option<u64>,
401 },
402 FilteredHistory {
403 worktree: Worktree,
404 refs: Vec<String>,
405 cursor: Option<String>,
406 filter: c::HistoryFilter,
407 },
408 LeaseObserve {
409 owner: LeaseOwner,
410 },
411 FileResolve {
412 root: File,
413 purpose: ResolvePurpose,
414 },
415 TerminalOpen {
416 lease_id: String,
417 session_id: String,
418 directory_base64: String,
419 shell_base64: String,
420 columns: u16,
421 rows: u16,
422 },
423 TerminalAttach {
424 lease_id: String,
425 session_id: String,
426 last_output_sequence: u64,
427 },
428 TerminalInput {
429 lease_id: String,
430 session_id: String,
431 },
432 TerminalResize {
433 lease_id: String,
434 session_id: String,
435 columns: u16,
436 rows: u16,
437 },
438 TerminalEnd {
439 lease_id: String,
440 session_id: String,
441 intent: crate::terminal::EndIntent,
442 },
443 List {
444 file: File,
445 },
446 PathVersion {
447 file: File,
448 },
449 Watch {
450 file: File,
451 },
452 Search {
453 request: Search,
454 },
455 FileMutate {
456 file: File,
457 #[serde(deserialize_with = "crate::types::required_option")]
458 expected: Option<WireVersion>,
459 command: FileCommand,
460 },
461 Discover {
462 file: File,
463 },
464 Contains {
465 worktree: Worktree,
466 file: File,
467 },
468 GitRead {
469 worktree: Worktree,
470 query: c::GitRead,
471 },
472 Status {
473 worktree: Worktree,
474 },
475 Diff {
476 worktree: Worktree,
477 path_base64: String,
478 source: DiffSource,
479 },
480 History {
481 worktree: Worktree,
482 refs: Vec<String>,
483 #[serde(deserialize_with = "crate::types::required_option")]
484 cursor: Option<String>,
485 },
486 GitMutate {
487 worktree: Worktree,
488 expected: GitState,
489 command: GitCommand,
490 },
491 GitExisting {
492 worktree: Worktree,
493 expected: GitState,
494 command: GitCommand,
495 },
496 Inspect,
497}
498impl Call {
499 pub fn requires_repository(&self) -> bool {
500 matches!(
501 self,
502 Self::Repository { .. }
503 | Self::Commit { .. }
504 | Self::FilteredHistory { .. }
505 | Self::GitMutate {
506 command: GitCommand::Repository { .. },
507 ..
508 }
509 | Self::GitExisting {
510 command: GitCommand::Repository { .. },
511 ..
512 }
513 | Self::Diff {
514 source: DiffSource::Commit { .. },
515 ..
516 }
517 )
518 }
519
520 pub fn terminal_lease(&self) -> Option<&str> {
521 match self {
522 Self::TerminalOpen { lease_id, .. }
523 | Self::TerminalAttach { lease_id, .. }
524 | Self::TerminalInput { lease_id, .. }
525 | Self::TerminalResize { lease_id, .. }
526 | Self::TerminalEnd { lease_id, .. } => Some(lease_id),
527 _ => None,
528 }
529 }
530 pub fn effectful(&self) -> bool {
531 self.mutating()
532 || matches!(
533 self,
534 Self::TerminalInput { .. } | Self::TerminalResize { .. } | Self::TerminalEnd { .. }
535 )
536 }
537
538 pub fn unprepared_worktree_mutation(&self) -> bool {
539 match self {
540 Self::GitMutate { command, .. } => command.core().is_ok_and(|v| v.changes_worktree()),
541 _ => false,
542 }
543 }
544
545 pub fn mutating(&self) -> bool {
546 matches!(
547 self,
548 Self::FileMutate { .. } | Self::GitMutate { .. } | Self::TerminalOpen { .. }
549 )
550 }
551}
552#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
553#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
554pub enum DiffSource {
555 Index,
556 Worktree,
557 Commit { oid: String },
558}
559impl DiffSource {
560 pub fn from_core(s: &c::DiffSource) -> Self {
561 match s {
562 c::DiffSource::Index => Self::Index,
563 c::DiffSource::Worktree => Self::Worktree,
564 c::DiffSource::Commit(oid) => Self::Commit { oid: oid.clone() },
565 }
566 }
567 pub fn core(&self) -> c::DiffSource {
568 match self {
569 Self::Index => c::DiffSource::Index,
570 Self::Worktree => c::DiffSource::Worktree,
571 Self::Commit { oid } => c::DiffSource::Commit(oid.clone()),
572 }
573 }
574}
575#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
576#[serde(deny_unknown_fields)]
577pub struct Entry {
578 pub file: File,
579 pub display_name: String,
580 pub kind: String,
581}
582#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
583#[serde(deny_unknown_fields)]
584pub struct GitEntry {
585 pub path_base64: String,
586 #[serde(deserialize_with = "crate::types::required_option")]
587 pub previous_path: Option<String>,
588 pub status: String,
589}
590impl GitEntry {
591 pub fn from_core(e: &c::GitEntry) -> Result<Self, c::CoreError> {
592 Ok(Self {
593 path_base64: path(&e.path)?,
594 previous_path: e.previous_path.as_ref().map(path).transpose()?,
595 status: e.status.clone(),
596 })
597 }
598 pub fn core(&self) -> Result<c::GitEntry, c::CoreError> {
599 Ok(c::GitEntry {
600 path: target_path(&self.path_base64)?,
601 previous_path: self.previous_path.as_deref().map(target_path).transpose()?,
602 status: self.status.clone(),
603 })
604 }
605}
606#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
607#[serde(deny_unknown_fields)]
608pub struct Commit {
609 pub lane: u32,
610 pub parent_lanes: Vec<u32>,
611 pub author: String,
612 pub authored_at: String,
613 pub message: String,
614 pub oid: String,
615 pub parents: Vec<String>,
616 pub decorations: Vec<String>,
617 pub subject: String,
618}
619impl Commit {
620 pub fn from_core(c: &c::GraphCommit) -> Self {
621 Self {
622 lane: c.lane,
623 parent_lanes: c.parent_lanes.clone(),
624 author: c.author.clone(),
625 authored_at: c.authored_at.clone(),
626 message: c.message.clone(),
627 oid: c.oid.clone(),
628 parents: c.parents.clone(),
629 decorations: c.decorations.iter().map(|b| B64.encode(b)).collect(),
630 subject: c.subject.clone(),
631 }
632 }
633 pub fn core(&self) -> Result<c::GraphCommit, c::CoreError> {
634 Ok(c::GraphCommit {
635 lane: self.lane,
636 parent_lanes: self.parent_lanes.clone(),
637 author: self.author.clone(),
638 authored_at: self.authored_at.clone(),
639 message: self.message.clone(),
640 oid: self.oid.clone(),
641 parents: self.parents.clone(),
642 decorations: self
643 .decorations
644 .iter()
645 .map(|b| B64.decode(b).map_err(|_| invalid()))
646 .collect::<Result<_, _>>()?,
647 subject: self.subject.clone(),
648 })
649 }
650}
651#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
652#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
653pub enum Reply {
654 Repository {
655 snapshot: Box<c::GitRepositorySnapshot>,
656 },
657 Commit {
658 page: c::CommitPage,
659 },
660 GitRead {
661 stdout_base64: String,
662 stderr_base64: String,
663 success: bool,
664 },
665 LeaseObserved {
666 owner: LeaseOwner,
667 runtime_generation: String,
668 presence: OwnerPresence,
669 workers_retired: bool,
670 },
671 FileResolved {
672 root: File,
673 file: File,
674 resolved_path: String,
675 #[serde(deserialize_with = "crate::types::required_option")]
676 node_kind: Option<String>,
677 #[serde(deserialize_with = "crate::types::required_option")]
678 version: Option<WireVersion>,
679 },
680 TerminalAck,
681 Terminal {
682 event: crate::terminal::Event,
683 },
684 List {
685 entries: Vec<Entry>,
686 },
687 Version {
688 version: WireVersion,
689 },
690 Watch {
691 file: File,
692 event: String,
693 },
694 SearchMatch {
695 file: File,
696 line: u64,
697 column_utf8: u64,
698 preview: String,
699 version: WireVersion,
700 digest: String,
701 },
702 SearchProgress {
703 files: u64,
704 matches: u64,
705 },
706 SearchComplete,
707 SearchCancelled,
708 SearchLimited {
709 maximum: u64,
710 },
711 Discovered {
712 #[serde(deserialize_with = "crate::types::required_option")]
713 worktree: Option<Worktree>,
714 },
715 Contains {
716 value: bool,
717 },
718 Status {
719 worktree: Worktree,
720 state: GitState,
721 staged: Vec<GitEntry>,
722 unstaged: Vec<GitEntry>,
723 untracked: Vec<GitEntry>,
724 conflicts: Vec<GitEntry>,
725 },
726 Diff {
727 source: DiffSource,
728 content: String,
729 #[serde(deserialize_with = "crate::types::required_option")]
730 unavailable: Option<String>,
731 change: String,
732 #[serde(deserialize_with = "crate::types::required_option")]
733 previous_path: Option<String>,
734 expected: GitState,
735 bytes_base64: String,
736 },
737 History {
738 commits: Vec<Commit>,
739 continuation_edges: Vec<(String, String)>,
740 #[serde(deserialize_with = "crate::types::required_option")]
741 next_cursor: Option<String>,
742 },
743 Receipt {
744 receipt: Receipt,
745 },
746 GitExisting {
747 #[serde(deserialize_with = "crate::types::required_option")]
748 receipt: Option<Receipt>,
749 },
750 Error {
751 code: String,
752 message: String,
753 },
754}
755impl Reply {
756 pub fn error(e: c::CoreError) -> Self {
757 Self::Error {
758 code: format!("{:?}", e.code),
759 message: e.message,
760 }
761 }
762 pub fn search(e: c::SearchEvent) -> Result<Self, c::CoreError> {
763 Ok(match e {
764 c::SearchEvent::Match(m) => Self::SearchMatch {
765 file: File::from_core(&m.file)?,
766 line: m.line,
767 column_utf8: m.preview_column_utf8,
768 preview: m.preview,
769 version: version(&m.searched_version),
770 digest: hex(&m.searched_digest),
771 },
772 c::SearchEvent::Progress { files, matches } => Self::SearchProgress { files, matches },
773 c::SearchEvent::Complete => Self::SearchComplete,
774 c::SearchEvent::Cancelled => Self::SearchCancelled,
775 c::SearchEvent::Limited { maximum } => Self::SearchLimited { maximum },
776 c::SearchEvent::Failed(e) => Self::error(e),
777 })
778 }
779}
780#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
781#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
782pub enum Observation {
783 Unobserved,
784 File {
785 file: File,
786 #[serde(deserialize_with = "crate::types::required_option")]
787 version: Option<WireVersion>,
788 #[serde(deserialize_with = "crate::types::required_option")]
789 digest: Option<String>,
790 },
791 Git {
792 worktree: Worktree,
793 state: GitState,
794 },
795 GitNetwork {
796 worktree: Worktree,
797 state: GitState,
798 intent: Box<NetworkIntent>,
799 #[serde(deserialize_with = "crate::types::required_option")]
800 observation: Option<NetworkObservation>,
801 },
802}
803#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
804#[serde(deny_unknown_fields)]
805pub struct NetworkIntent {
806 pub action: String,
807 pub remote: String,
808 pub reference: String,
809 #[serde(deserialize_with = "crate::types::required_option")]
810 pub endpoint_fingerprint: Option<String>,
811 pub push_mapping_unconfigured: bool,
812}
813#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
814#[serde(deny_unknown_fields)]
815pub struct NetworkObservation {
816 #[serde(deserialize_with = "crate::types::required_option")]
817 pub remote_oid: Option<String>,
818 pub remote_refs: String,
819 pub local_refs: String,
820 pub fetch_head: String,
821 pub fetch_head_oids: String,
822}
823impl Observation {
824 pub fn from_core(o: &c::ObservedState) -> Result<Self, c::CoreError> {
825 Ok(match o {
826 c::ObservedState::Unobserved => Self::Unobserved,
827 c::ObservedState::File {
828 file,
829 version: v,
830 digest: d,
831 } => Self::File {
832 file: File::from_core(file)?,
833 version: v.as_ref().map(version),
834 digest: d.as_ref().map(|b| hex(b)),
835 },
836 c::ObservedState::Git { worktree, state } => Self::Git {
837 worktree: Worktree::from_core(worktree)?,
838 state: GitState::from_core(state),
839 },
840 c::ObservedState::GitNetwork {
841 worktree,
842 state,
843 intent,
844 observation,
845 } => Self::GitNetwork {
846 worktree: Worktree::from_core(worktree)?,
847 state: GitState::from_core(state),
848 intent: Box::new(NetworkIntent {
849 action: format!("{:?}", intent.action),
850 remote: B64.encode(&intent.remote),
851 reference: B64.encode(&intent.reference),
852 endpoint_fingerprint: intent.endpoint_fingerprint.as_ref().map(|b| hex(b)),
853 push_mapping_unconfigured: intent.push_mapping_unconfigured,
854 }),
855 observation: observation.as_ref().map(|o| NetworkObservation {
856 remote_oid: o.remote_oid.clone(),
857 remote_refs: hex(&o.remote_refs_fingerprint),
858 local_refs: hex(&o.local_refs_fingerprint),
859 fetch_head: hex(&o.fetch_head_fingerprint),
860 fetch_head_oids: B64.encode(&o.fetch_head_oids),
861 }),
862 },
863 _ => return Err(invalid()),
864 })
865 }
866 pub fn core(&self, t: &c::TargetIdentity) -> Result<c::ObservedState, c::CoreError> {
867 Ok(match self {
868 Self::Unobserved => c::ObservedState::Unobserved,
869 Self::File {
870 file,
871 version,
872 digest: d,
873 } => c::ObservedState::File {
874 file: file.core(t)?,
875 version: version.as_ref().map(|v| v.core(t)).transpose()?,
876 digest: d.as_deref().map(digest).transpose()?,
877 },
878 Self::Git { worktree, state } => c::ObservedState::Git {
879 worktree: worktree.core(t)?,
880 state: state.core()?,
881 },
882 Self::GitNetwork {
883 worktree,
884 state,
885 intent: i,
886 observation: o,
887 } => c::ObservedState::GitNetwork {
888 worktree: worktree.core(t)?,
889 state: state.core()?,
890 intent: c::GitNetworkIntent {
891 action: match i.action.as_str() {
892 "Fetch" => c::GitNetworkAction::Fetch,
893 "Push" => c::GitNetworkAction::Push,
894 "PullFastForward" => c::GitNetworkAction::PullFastForward,
895 _ => return Err(invalid()),
896 },
897 remote: B64.decode(&i.remote).map_err(|_| invalid())?,
898 reference: B64.decode(&i.reference).map_err(|_| invalid())?,
899 endpoint_fingerprint: i
900 .endpoint_fingerprint
901 .as_deref()
902 .map(digest)
903 .transpose()?,
904 push_mapping_unconfigured: i.push_mapping_unconfigured,
905 },
906 observation: o
907 .as_ref()
908 .map(|o| {
909 Ok(c::GitNetworkObservation {
910 remote_oid: o.remote_oid.clone(),
911 remote_refs_fingerprint: digest(&o.remote_refs)?,
912 local_refs_fingerprint: digest(&o.local_refs)?,
913 fetch_head_fingerprint: digest(&o.fetch_head)?,
914 fetch_head_oids: B64
915 .decode(&o.fetch_head_oids)
916 .map_err(|_| invalid())?,
917 })
918 })
919 .transpose()?,
920 },
921 })
922 }
923}
924#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
925#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
926pub enum ResultValue {
927 TerminalOpened {
928 session_id: String,
929 runtime_generation: String,
930 #[serde(deserialize_with = "crate::types::required_option")]
931 container_start_base64: Option<String>,
932 },
933 Saved {
934 revision: u64,
935 version: WireVersion,
936 },
937 FileChanged {
938 file: File,
939 #[serde(deserialize_with = "crate::types::required_option")]
940 version: Option<WireVersion>,
941 },
942 GitChanged {
943 worktree: Worktree,
944 state: GitState,
945 #[serde(default, skip_serializing_if = "Option::is_none")]
946 outcome: Option<Box<c::RepositoryOutcome>>,
947 },
948}
949#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
950#[serde(deny_unknown_fields)]
951pub struct Receipt {
952 pub operation_id: String,
953 pub state: String,
954 #[serde(deserialize_with = "crate::types::required_option")]
955 pub result: Option<ResultValue>,
956 #[serde(deserialize_with = "crate::types::required_option")]
957 pub code: Option<String>,
958 pub message: String,
959 pub possible_effects: bool,
960 pub observed: Box<Observation>,
961}
962impl Receipt {
963 pub fn from_core(r: &c::OperationReceipt) -> Result<Self, c::CoreError> {
964 let mut w = Self {
965 operation_id: uuid(r.id.0),
966 state: String::new(),
967 result: None,
968 code: None,
969 message: String::new(),
970 possible_effects: false,
971 observed: Box::new(Observation::Unobserved),
972 };
973 w.state = match &r.state {
974 c::OperationState::Accepted => "accepted",
975 c::OperationState::Running => "running",
976 c::OperationState::Confirmed(r) => {
977 w.result = Some(match r {
978 c::MutationResult::Saved(s) => ResultValue::Saved {
979 revision: s.saved_revision.0,
980 version: version(&s.version),
981 },
982 c::MutationResult::FileChanged { file, version: v } => {
983 ResultValue::FileChanged {
984 file: File::from_core(file)?,
985 version: v.as_ref().map(version),
986 }
987 }
988 c::MutationResult::GitChanged {
989 worktree,
990 state,
991 outcome,
992 } => ResultValue::GitChanged {
993 outcome: outcome.clone(),
994 worktree: Worktree::from_core(worktree)?,
995 state: GitState::from_core(state),
996 },
997 c::MutationResult::TerminalOpened { session, runtime } => {
998 ResultValue::TerminalOpened {
999 session_id: uuid(session.0),
1000 runtime_generation: uuid(runtime.supervisor_boot),
1001 container_start_base64: runtime
1002 .container_start
1003 .as_ref()
1004 .map(|s| B64.encode(s)),
1005 }
1006 }
1007 });
1008 "confirmed"
1009 }
1010 c::OperationState::Failed(f) => {
1011 w.code = Some(format!("{:?}", f.error.code));
1012 w.message = f.error.message.clone();
1013 w.possible_effects = f.effect == c::FailureEffect::PossiblePartialEffects;
1014 w.observed = Box::new(Observation::from_core(&f.observed)?);
1015 "failed"
1016 }
1017 c::OperationState::Unknown(e) => {
1018 w.message = e.explanation.clone();
1019 w.possible_effects = true;
1020 w.observed = Box::new(Observation::from_core(&e.observed)?);
1021 "unknown"
1022 }
1023 }
1024 .into();
1025 Ok(w)
1026 }
1027 pub fn core(&self, t: &c::TargetIdentity) -> Result<c::OperationReceipt, c::CoreError> {
1028 Ok(c::OperationReceipt {
1029 id: c::OperationID(id(&self.operation_id)?),
1030 state: match self.state.as_str() {
1031 "accepted" => c::OperationState::Accepted,
1032 "running" => c::OperationState::Running,
1033 "confirmed" => c::OperationState::Confirmed(
1034 match self.result.as_ref().ok_or_else(invalid)? {
1035 ResultValue::TerminalOpened {
1036 session_id,
1037 runtime_generation,
1038 container_start_base64,
1039 } => c::MutationResult::TerminalOpened {
1040 session: c::TerminalSessionID(id(session_id)?),
1041 runtime: c::RuntimeGeneration {
1042 supervisor_boot: id(runtime_generation)?,
1043 container_start: container_start_base64
1044 .as_ref()
1045 .map(|s| B64.decode(s).map_err(|_| invalid()))
1046 .transpose()?,
1047 },
1048 },
1049 ResultValue::Saved { revision, version } => {
1050 c::MutationResult::Saved(c::SaveResult {
1051 saved_revision: c::BufferRevision(*revision),
1052 version: version.core(t)?,
1053 })
1054 }
1055 ResultValue::FileChanged { file, version } => {
1056 c::MutationResult::FileChanged {
1057 file: file.core(t)?,
1058 version: version.as_ref().map(|v| v.core(t)).transpose()?,
1059 }
1060 }
1061 ResultValue::GitChanged {
1062 worktree,
1063 state,
1064 outcome,
1065 } => c::MutationResult::GitChanged {
1066 outcome: outcome.clone(),
1067 worktree: worktree.core(t)?,
1068 state: state.core()?,
1069 },
1070 },
1071 ),
1072 "failed" => c::OperationState::Failed(c::OperationFailure {
1073 error: core_error(self.code.as_deref().ok_or_else(invalid)?, &self.message),
1074 target: t.clone(),
1075 observed: self.observed.core(t)?,
1076 effect: if self.possible_effects {
1077 c::FailureEffect::PossiblePartialEffects
1078 } else {
1079 c::FailureEffect::ProvenNoEffect
1080 },
1081 }),
1082 "unknown" => c::OperationState::Unknown(c::ReconciliationEvidence {
1083 target: t.clone(),
1084 observed: self.observed.core(t)?,
1085 explanation: self.message.clone(),
1086 }),
1087 _ => return Err(invalid()),
1088 },
1089 })
1090 }
1091}
1092pub fn core_error(code: &str, message: &str) -> c::CoreError {
1093 use c::ErrorCode::*;
1094 c::CoreError::new(
1095 match code {
1096 "InvalidArgument" | "invalid_argument" => InvalidArgument,
1097 "StaleRevision" => StaleRevision,
1098 "Unavailable" | "unavailable" => Unavailable,
1099 "Busy" | "busy" => Busy,
1100 "Cancelled" => Cancelled,
1101 "Unknown" => Unknown,
1102 "TargetChanged" => TargetChanged,
1103 "Closed" => Closed,
1104 "Io" => Io,
1105 _ => Unknown,
1106 },
1107 message,
1108 )
1109}
1110
1111#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1112#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1113pub enum ResolvePurpose {
1114 Root,
1115 OpenChild { path_base64: String },
1116 MutationNode { path_base64: String },
1117}
1118impl ResolvePurpose {
1119 pub fn from_core(value: &c::FileResolvePurpose) -> Result<Self, c::CoreError> {
1120 Ok(match value {
1121 c::FileResolvePurpose::Root => Self::Root,
1122 c::FileResolvePurpose::OpenChild(p) => Self::OpenChild {
1123 path_base64: path(p)?,
1124 },
1125 c::FileResolvePurpose::MutationNode(p) => Self::MutationNode {
1126 path_base64: path(p)?,
1127 },
1128 })
1129 }
1130 pub fn core(&self) -> Result<c::FileResolvePurpose, c::CoreError> {
1131 Ok(match self {
1132 Self::Root => c::FileResolvePurpose::Root,
1133 Self::OpenChild { path_base64 } => {
1134 c::FileResolvePurpose::OpenChild(target_path(path_base64)?)
1135 }
1136 Self::MutationNode { path_base64 } => {
1137 c::FileResolvePurpose::MutationNode(target_path(path_base64)?)
1138 }
1139 })
1140 }
1141}