1pub mod dialect;
7
8pub use dialect::Backend;
9
10use serde::{Deserialize, Serialize};
11use std::fmt;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum BranchResult {
16 Success,
18 Failure,
20 Ongoing,
22 Unknown,
27}
28
29impl BranchResult {
30 pub fn from_http(status: u16, body: &str) -> Self {
33 if body.contains("FAILURE") {
34 return Self::Failure;
35 }
36 if body.contains("ONGOING") {
37 return Self::Ongoing;
38 }
39 match status {
40 200..=299 => Self::Success,
41 409 => Self::Failure,
42 425 => Self::Ongoing,
43 _ => Self::Unknown,
44 }
45 }
46
47 pub fn from_grpc(code: i32) -> Self {
69 match code {
70 GRPC_OK => Self::Success,
71 GRPC_ABORTED => Self::Failure,
72 GRPC_FAILED_PRECONDITION => Self::Ongoing,
73 _ => Self::Unknown,
76 }
77 }
78}
79
80pub const GRPC_OK: i32 = 0;
82pub const GRPC_ABORTED: i32 = 10;
84pub const GRPC_FAILED_PRECONDITION: i32 = 9;
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "lowercase")]
89pub enum TransType {
90 Saga,
91 Tcc,
92 Msg,
93 Xa,
94 Workflow,
97}
98
99impl fmt::Display for TransType {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 let s = match self {
102 Self::Saga => "saga",
103 Self::Tcc => "tcc",
104 Self::Msg => "msg",
105 Self::Xa => "xa",
106 Self::Workflow => "workflow",
107 };
108 f.write_str(s)
109 }
110}
111
112impl TransType {
113 pub fn parse(s: &str) -> Option<Self> {
114 match s {
115 "saga" => Some(Self::Saga),
116 "tcc" => Some(Self::Tcc),
117 "msg" => Some(Self::Msg),
118 "xa" => Some(Self::Xa),
119 "workflow" => Some(Self::Workflow),
120 _ => None,
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "lowercase")]
127pub enum GlobalStatus {
128 Prepared,
130 Submitted,
132 Aborting,
134 Succeed,
136 Failed,
138}
139
140impl GlobalStatus {
141 pub fn as_str(&self) -> &'static str {
142 match self {
143 Self::Prepared => "prepared",
144 Self::Submitted => "submitted",
145 Self::Aborting => "aborting",
146 Self::Succeed => "succeed",
147 Self::Failed => "failed",
148 }
149 }
150
151 pub fn parse(s: &str) -> Option<Self> {
152 match s {
153 "prepared" => Some(Self::Prepared),
154 "submitted" => Some(Self::Submitted),
155 "aborting" => Some(Self::Aborting),
156 "succeed" => Some(Self::Succeed),
157 "failed" => Some(Self::Failed),
158 _ => None,
159 }
160 }
161
162 pub fn is_final(&self) -> bool {
164 matches!(self, Self::Succeed | Self::Failed)
165 }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "lowercase")]
170pub enum BranchStatus {
171 Prepared,
172 Succeed,
173 Failed,
174}
175
176impl BranchStatus {
177 pub fn as_str(&self) -> &'static str {
178 match self {
179 Self::Prepared => "prepared",
180 Self::Succeed => "succeed",
181 Self::Failed => "failed",
182 }
183 }
184 pub fn parse(s: &str) -> Option<Self> {
185 match s {
186 "prepared" => Some(Self::Prepared),
187 "succeed" => Some(Self::Succeed),
188 "failed" => Some(Self::Failed),
189 _ => None,
190 }
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(rename_all = "lowercase")]
197pub enum BranchOp {
198 Action,
199 Compensate,
200 Try,
201 Confirm,
202 Cancel,
203 Commit,
205 Rollback,
206}
207
208impl BranchOp {
209 pub fn as_str(&self) -> &'static str {
210 match self {
211 Self::Action => "action",
212 Self::Compensate => "compensate",
213 Self::Try => "try",
214 Self::Confirm => "confirm",
215 Self::Cancel => "cancel",
216 Self::Commit => "commit",
217 Self::Rollback => "rollback",
218 }
219 }
220
221 pub fn parse(s: &str) -> Option<Self> {
222 match s {
223 "action" => Some(Self::Action),
224 "compensate" => Some(Self::Compensate),
225 "try" => Some(Self::Try),
226 "confirm" => Some(Self::Confirm),
227 "cancel" => Some(Self::Cancel),
228 "commit" => Some(Self::Commit),
229 "rollback" => Some(Self::Rollback),
230 _ => None,
231 }
232 }
233
234 pub fn origin_op(&self) -> Option<BranchOp> {
236 match self {
237 Self::Cancel => Some(Self::Try),
238 Self::Compensate => Some(Self::Action),
239 Self::Rollback => Some(Self::Action),
240 _ => None,
241 }
242 }
243
244 pub fn is_compensating(&self) -> bool {
245 self.origin_op().is_some()
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct SagaStep {
252 pub action: String,
253 pub compensate: String,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
258pub enum Advance {
259 Call { index: usize, op: BranchOp },
261 Finish(GlobalStatus),
263 Wait,
265 RunWorkflow,
270}
271
272pub fn saga_advance(
276 status: GlobalStatus,
277 actions: &[BranchStatus],
278 compensates: &[BranchStatus],
279) -> Advance {
280 debug_assert_eq!(actions.len(), compensates.len());
281 match status {
282 GlobalStatus::Submitted => {
283 for (i, st) in actions.iter().enumerate() {
285 match st {
286 BranchStatus::Succeed => continue,
287 BranchStatus::Prepared => {
288 return Advance::Call {
289 index: i,
290 op: BranchOp::Action,
291 }
292 }
293 BranchStatus::Failed => return Advance::Finish(GlobalStatus::Aborting),
295 }
296 }
297 Advance::Finish(GlobalStatus::Succeed)
298 }
299 GlobalStatus::Aborting => {
300 for i in (0..compensates.len()).rev() {
303 if compensates[i] == BranchStatus::Prepared {
304 return Advance::Call {
305 index: i,
306 op: BranchOp::Compensate,
307 };
308 }
309 }
310 Advance::Finish(GlobalStatus::Failed)
311 }
312 GlobalStatus::Prepared => Advance::Wait,
313 s => Advance::Finish(s),
314 }
315}
316
317pub fn next_interval(cur: i64) -> i64 {
319 const MAX: i64 = 300;
320 if cur <= 0 {
321 return 10;
322 }
323 (cur * 2).min(MAX)
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use BranchStatus::{Failed, Prepared, Succeed};
330
331 #[test]
332 fn 超时不能当失败() {
333 assert_eq!(BranchResult::from_http(504, ""), BranchResult::Unknown);
335 assert_eq!(BranchResult::from_http(500, ""), BranchResult::Unknown);
336 assert_eq!(BranchResult::from_http(409, ""), BranchResult::Failure);
338 assert_eq!(
339 BranchResult::from_http(200, r#"{"dtm_result":"FAILURE"}"#),
340 BranchResult::Failure
341 );
342 assert_eq!(BranchResult::from_http(425, ""), BranchResult::Ongoing);
343 assert_eq!(BranchResult::from_http(200, "ok"), BranchResult::Success);
344 }
345
346 #[test]
347 fn grpc只有aborted才算失败() {
348 assert_eq!(BranchResult::from_grpc(0), BranchResult::Success);
349 assert_eq!(BranchResult::from_grpc(10), BranchResult::Failure);
351 assert_eq!(BranchResult::from_grpc(9), BranchResult::Ongoing);
352
353 for code in 0..=15 {
357 let want = match code {
358 0 => BranchResult::Success,
359 10 => BranchResult::Failure,
360 9 => BranchResult::Ongoing,
361 _ => BranchResult::Unknown,
362 };
363 assert_eq!(BranchResult::from_grpc(code), want, "gRPC 码 {code} 判错了");
364 }
365 assert_eq!(
367 BranchResult::from_grpc(1),
368 BranchResult::Unknown,
369 "CANCELLED 是我们自己放弃,不是对方拒绝"
370 );
371 assert_eq!(
372 BranchResult::from_grpc(4),
373 BranchResult::Unknown,
374 "DEADLINE_EXCEEDED 绝不能当失败"
375 );
376 assert_eq!(
377 BranchResult::from_grpc(14),
378 BranchResult::Unknown,
379 "UNAVAILABLE 绝不能当失败"
380 );
381 assert_eq!(BranchResult::from_grpc(99), BranchResult::Unknown);
383 assert_eq!(BranchResult::from_grpc(-1), BranchResult::Unknown);
384 }
385
386 #[test]
387 fn grpc与http的判定语义一致() {
388 for (http, grpc) in [(200u16, 0i32), (409, 10), (425, 9), (500, 13), (503, 14)] {
391 assert_eq!(
392 BranchResult::from_http(http, ""),
393 BranchResult::from_grpc(grpc),
394 "HTTP {http} 与 gRPC {grpc} 应当判定一致"
395 );
396 }
397 }
398
399 #[test]
400 fn 正向按序推进() {
401 let a = [Prepared, Prepared];
402 let c = [Prepared, Prepared];
403 assert_eq!(
404 saga_advance(GlobalStatus::Submitted, &a, &c),
405 Advance::Call {
406 index: 0,
407 op: BranchOp::Action
408 }
409 );
410 let a = [Succeed, Prepared];
411 assert_eq!(
412 saga_advance(GlobalStatus::Submitted, &a, &c),
413 Advance::Call {
414 index: 1,
415 op: BranchOp::Action
416 }
417 );
418 let a = [Succeed, Succeed];
419 assert_eq!(
420 saga_advance(GlobalStatus::Submitted, &a, &c),
421 Advance::Finish(GlobalStatus::Succeed)
422 );
423 }
424
425 #[test]
426 fn 补偿必须逆序() {
427 let a = [Succeed, Failed];
428 let c = [Prepared, Prepared];
429 assert_eq!(
431 saga_advance(GlobalStatus::Aborting, &a, &c),
432 Advance::Call {
433 index: 1,
434 op: BranchOp::Compensate
435 }
436 );
437 let c = [Prepared, Succeed];
438 assert_eq!(
439 saga_advance(GlobalStatus::Aborting, &a, &c),
440 Advance::Call {
441 index: 0,
442 op: BranchOp::Compensate
443 }
444 );
445 let c = [Succeed, Succeed];
446 assert_eq!(
447 saga_advance(GlobalStatus::Aborting, &a, &c),
448 Advance::Finish(GlobalStatus::Failed)
449 );
450 }
451
452 #[test]
453 fn 没跑过的分支也要补偿() {
454 let a = [Prepared, Prepared];
457 let c = [Prepared, Prepared];
458 assert_eq!(
459 saga_advance(GlobalStatus::Aborting, &a, &c),
460 Advance::Call {
461 index: 1,
462 op: BranchOp::Compensate
463 }
464 );
465 }
466
467 #[test]
468 fn 终态不再推进() {
469 for s in [GlobalStatus::Succeed, GlobalStatus::Failed] {
470 assert!(s.is_final());
471 assert_eq!(saga_advance(s, &[], &[]), Advance::Finish(s));
472 }
473 }
474
475 #[test]
476 fn 补偿操作能找到正向操作() {
477 assert_eq!(BranchOp::Compensate.origin_op(), Some(BranchOp::Action));
478 assert_eq!(BranchOp::Cancel.origin_op(), Some(BranchOp::Try));
479 assert_eq!(BranchOp::Action.origin_op(), None);
480 assert!(BranchOp::Compensate.is_compensating());
481 assert!(!BranchOp::Try.is_compensating());
482 }
483
484 #[test]
485 fn 退避有上限() {
486 assert_eq!(next_interval(0), 10);
487 assert_eq!(next_interval(10), 20);
488 assert_eq!(next_interval(200), 300);
489 assert_eq!(next_interval(300), 300);
490 }
491}
492
493pub fn tcc_advance(
507 status: GlobalStatus,
508 confirms: &[BranchStatus],
509 cancels: &[BranchStatus],
510) -> Advance {
511 debug_assert_eq!(confirms.len(), cancels.len());
512 match status {
513 GlobalStatus::Prepared => Advance::Wait,
515 GlobalStatus::Submitted => {
516 for (i, st) in confirms.iter().enumerate() {
517 match st {
518 BranchStatus::Succeed => continue,
519 BranchStatus::Prepared | BranchStatus::Failed => {
521 return Advance::Call {
522 index: i,
523 op: BranchOp::Confirm,
524 }
525 }
526 }
527 }
528 Advance::Finish(GlobalStatus::Succeed)
529 }
530 GlobalStatus::Aborting => {
531 for i in (0..cancels.len()).rev() {
533 if cancels[i] != BranchStatus::Succeed {
534 return Advance::Call {
535 index: i,
536 op: BranchOp::Cancel,
537 };
538 }
539 }
540 Advance::Finish(GlobalStatus::Failed)
541 }
542 s => Advance::Finish(s),
543 }
544}
545
546pub fn msg_advance(status: GlobalStatus, actions: &[BranchStatus]) -> Advance {
560 match status {
561 GlobalStatus::Prepared => Advance::Wait,
563 GlobalStatus::Submitted => {
564 for (i, st) in actions.iter().enumerate() {
565 if *st != BranchStatus::Succeed {
566 return Advance::Call {
567 index: i,
568 op: BranchOp::Action,
569 };
570 }
571 }
572 Advance::Finish(GlobalStatus::Succeed)
573 }
574 GlobalStatus::Aborting => Advance::Finish(GlobalStatus::Failed),
576 s => Advance::Finish(s),
577 }
578}
579
580#[cfg(test)]
581mod tcc_msg_tests {
582 use super::*;
583 use BranchStatus::{Failed, Prepared, Succeed};
584
585 #[test]
586 fn tcc的try阶段tc不插手() {
587 assert_eq!(
588 tcc_advance(GlobalStatus::Prepared, &[Prepared], &[Prepared]),
589 Advance::Wait
590 );
591 }
592
593 #[test]
594 fn tcc按序confirm() {
595 let c = [Prepared, Prepared];
596 let x = [Prepared, Prepared];
597 assert_eq!(
598 tcc_advance(GlobalStatus::Submitted, &c, &x),
599 Advance::Call {
600 index: 0,
601 op: BranchOp::Confirm
602 }
603 );
604 assert_eq!(
605 tcc_advance(GlobalStatus::Submitted, &[Succeed, Prepared], &x),
606 Advance::Call {
607 index: 1,
608 op: BranchOp::Confirm
609 }
610 );
611 assert_eq!(
612 tcc_advance(GlobalStatus::Submitted, &[Succeed, Succeed], &x),
613 Advance::Finish(GlobalStatus::Succeed)
614 );
615 }
616
617 #[test]
618 fn confirm失败绝不能触发cancel() {
619 let c = [Succeed, Failed];
622 let x = [Prepared, Prepared];
623 assert_eq!(
624 tcc_advance(GlobalStatus::Submitted, &c, &x),
625 Advance::Call {
626 index: 1,
627 op: BranchOp::Confirm
628 },
629 "confirm 失败要继续重试 confirm,不能转 cancel"
630 );
631 for a in [Prepared, Succeed, Failed] {
633 for b in [Prepared, Succeed, Failed] {
634 let r = tcc_advance(GlobalStatus::Submitted, &[a, b], &x);
635 assert_ne!(r, Advance::Finish(GlobalStatus::Aborting));
636 assert_ne!(r, Advance::Finish(GlobalStatus::Failed));
637 }
638 }
639 }
640
641 #[test]
642 fn tcc逆序cancel() {
643 let c = [Prepared, Prepared];
644 assert_eq!(
645 tcc_advance(GlobalStatus::Aborting, &c, &[Prepared, Prepared]),
646 Advance::Call {
647 index: 1,
648 op: BranchOp::Cancel
649 }
650 );
651 assert_eq!(
652 tcc_advance(GlobalStatus::Aborting, &c, &[Prepared, Succeed]),
653 Advance::Call {
654 index: 0,
655 op: BranchOp::Cancel
656 }
657 );
658 assert_eq!(
659 tcc_advance(GlobalStatus::Aborting, &c, &[Succeed, Succeed]),
660 Advance::Finish(GlobalStatus::Failed)
661 );
662 assert_eq!(
664 tcc_advance(GlobalStatus::Aborting, &c, &[Succeed, Failed]),
665 Advance::Call {
666 index: 1,
667 op: BranchOp::Cancel
668 }
669 );
670 }
671
672 #[test]
673 fn msg等回查而不是自己推() {
674 assert_eq!(
675 msg_advance(GlobalStatus::Prepared, &[Prepared]),
676 Advance::Wait
677 );
678 }
679
680 #[test]
681 fn msg只往前不补偿() {
682 assert_eq!(
683 msg_advance(GlobalStatus::Submitted, &[Prepared, Prepared]),
684 Advance::Call {
685 index: 0,
686 op: BranchOp::Action
687 }
688 );
689 assert_eq!(
691 msg_advance(GlobalStatus::Submitted, &[Failed]),
692 Advance::Call {
693 index: 0,
694 op: BranchOp::Action
695 }
696 );
697 assert_eq!(
698 msg_advance(GlobalStatus::Submitted, &[Succeed, Succeed]),
699 Advance::Finish(GlobalStatus::Succeed)
700 );
701 assert_eq!(
703 msg_advance(GlobalStatus::Aborting, &[Succeed]),
704 Advance::Finish(GlobalStatus::Failed)
705 );
706 }
707}
708
709pub fn xa_advance(
725 status: GlobalStatus,
726 commits: &[BranchStatus],
727 rollbacks: &[BranchStatus],
728) -> Advance {
729 debug_assert_eq!(commits.len(), rollbacks.len());
730 match status {
731 GlobalStatus::Prepared => Advance::Wait,
733 GlobalStatus::Submitted => {
734 for (i, st) in commits.iter().enumerate() {
735 if *st != BranchStatus::Succeed {
736 return Advance::Call {
737 index: i,
738 op: BranchOp::Commit,
739 };
740 }
741 }
742 Advance::Finish(GlobalStatus::Succeed)
743 }
744 GlobalStatus::Aborting => {
745 for i in (0..rollbacks.len()).rev() {
746 if rollbacks[i] != BranchStatus::Succeed {
747 return Advance::Call {
748 index: i,
749 op: BranchOp::Rollback,
750 };
751 }
752 }
753 Advance::Finish(GlobalStatus::Failed)
754 }
755 s => Advance::Finish(s),
756 }
757}
758
759pub fn workflow_advance(status: GlobalStatus, compensates: &[BranchStatus]) -> Advance {
788 match status {
789 GlobalStatus::Prepared => Advance::Wait,
791 GlobalStatus::Submitted => Advance::RunWorkflow,
792 GlobalStatus::Aborting => {
793 for i in (0..compensates.len()).rev() {
795 if compensates[i] != BranchStatus::Succeed {
796 return Advance::Call {
797 index: i,
798 op: BranchOp::Compensate,
799 };
800 }
801 }
802 Advance::Finish(GlobalStatus::Failed)
803 }
804 s => Advance::Finish(s),
805 }
806}
807
808#[cfg(test)]
809mod workflow_tests {
810 use super::*;
811 use BranchStatus::{Failed, Prepared, Succeed};
812
813 #[test]
814 fn submitted就是去跑函数() {
815 assert_eq!(
816 workflow_advance(GlobalStatus::Submitted, &[]),
817 Advance::RunWorkflow
818 );
819 assert_eq!(
822 workflow_advance(GlobalStatus::Submitted, &[Succeed, Prepared]),
823 Advance::RunWorkflow
824 );
825 }
826
827 #[test]
828 fn 回滚时逆序补偿() {
829 assert_eq!(
830 workflow_advance(GlobalStatus::Aborting, &[Prepared, Prepared]),
831 Advance::Call {
832 index: 1,
833 op: BranchOp::Compensate
834 },
835 "后执行的先回滚"
836 );
837 assert_eq!(
838 workflow_advance(GlobalStatus::Aborting, &[Prepared, Succeed]),
839 Advance::Call {
840 index: 0,
841 op: BranchOp::Compensate
842 }
843 );
844 assert_eq!(
845 workflow_advance(GlobalStatus::Aborting, &[Succeed, Succeed]),
846 Advance::Finish(GlobalStatus::Failed)
847 );
848 assert_eq!(
850 workflow_advance(GlobalStatus::Aborting, &[Succeed, Failed]),
851 Advance::Call {
852 index: 1,
853 op: BranchOp::Compensate
854 }
855 );
856 }
857
858 #[test]
859 fn 一个分支都没登记就回滚是直接失败() {
860 assert_eq!(
862 workflow_advance(GlobalStatus::Aborting, &[]),
863 Advance::Finish(GlobalStatus::Failed)
864 );
865 }
866
867 #[test]
868 fn 终态不再推进() {
869 for s in [GlobalStatus::Succeed, GlobalStatus::Failed] {
870 assert_eq!(workflow_advance(s, &[]), Advance::Finish(s));
871 }
872 }
873
874 #[test]
875 fn workflow是一种事务类型() {
876 assert_eq!(TransType::parse("workflow"), Some(TransType::Workflow));
877 assert_eq!(TransType::Workflow.to_string(), "workflow");
878 }
879}
880
881#[cfg(test)]
882mod xa_tests {
883 use super::*;
884 use BranchStatus::{Failed, Prepared, Succeed};
885
886 #[test]
887 fn xa的prepare阶段tc不插手() {
888 assert_eq!(
890 xa_advance(GlobalStatus::Prepared, &[Prepared], &[Prepared]),
891 Advance::Wait
892 );
893 }
894
895 #[test]
896 fn xa按序commit() {
897 let r = [Prepared, Prepared];
898 assert_eq!(
899 xa_advance(GlobalStatus::Submitted, &[Prepared, Prepared], &r),
900 Advance::Call {
901 index: 0,
902 op: BranchOp::Commit
903 }
904 );
905 assert_eq!(
906 xa_advance(GlobalStatus::Submitted, &[Succeed, Prepared], &r),
907 Advance::Call {
908 index: 1,
909 op: BranchOp::Commit
910 }
911 );
912 assert_eq!(
913 xa_advance(GlobalStatus::Submitted, &[Succeed, Succeed], &r),
914 Advance::Finish(GlobalStatus::Succeed)
915 );
916 }
917
918 #[test]
919 fn commit失败绝不能转rollback() {
920 let r = [Prepared, Prepared];
922 assert_eq!(
923 xa_advance(GlobalStatus::Submitted, &[Succeed, Failed], &r),
924 Advance::Call {
925 index: 1,
926 op: BranchOp::Commit
927 },
928 "commit 失败要继续重试 commit"
929 );
930 for a in [Prepared, Succeed, Failed] {
932 for b in [Prepared, Succeed, Failed] {
933 let got = xa_advance(GlobalStatus::Submitted, &[a, b], &r);
934 assert_ne!(got, Advance::Finish(GlobalStatus::Aborting));
935 assert_ne!(got, Advance::Finish(GlobalStatus::Failed));
936 }
937 }
938 }
939
940 #[test]
941 fn xa逆序rollback且失败也要重试() {
942 let c = [Prepared, Prepared];
943 assert_eq!(
944 xa_advance(GlobalStatus::Aborting, &c, &[Prepared, Prepared]),
945 Advance::Call {
946 index: 1,
947 op: BranchOp::Rollback
948 }
949 );
950 assert_eq!(
951 xa_advance(GlobalStatus::Aborting, &c, &[Succeed, Succeed]),
952 Advance::Finish(GlobalStatus::Failed)
953 );
954 assert_eq!(
956 xa_advance(GlobalStatus::Aborting, &c, &[Succeed, Failed]),
957 Advance::Call {
958 index: 1,
959 op: BranchOp::Rollback
960 }
961 );
962 }
963
964 #[test]
965 fn commit操作没有反向映射() {
966 assert_eq!(BranchOp::Commit.origin_op(), None);
968 assert!(!BranchOp::Commit.is_compensating());
969 assert_eq!(BranchOp::parse("commit"), Some(BranchOp::Commit));
970 assert_eq!(BranchOp::Commit.as_str(), "commit");
971 }
972}