1use std::ffi::{OsStr, OsString};
8use std::path::{Path, PathBuf};
9
10use crate::command_history::redaction::redact_argv;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct JjCommandSpec {
15 argv: Vec<OsString>,
16 global_options: GlobalOptions,
17 cwd: Option<PathBuf>,
18 stdin: Option<String>,
19 title: String,
20 mode: ExecutionMode,
21 safety: SafetyClass,
22 refresh_plan: RefreshPlan,
23}
24
25impl JjCommandSpec {
26 #[must_use]
28 pub fn render_read_only(argv: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
29 let argv = argv.into_iter().map(Into::into).collect::<Vec<_>>();
30 let title = preview_argv(&argv);
31 Self {
32 argv,
33 global_options: GlobalOptions::default(),
34 cwd: None,
35 stdin: None,
36 title,
37 mode: ExecutionMode::RenderReadOnly,
38 safety: SafetyClass::ReadOnly,
39 refresh_plan: RefreshPlan::ReRunSpec,
40 }
41 }
42
43 #[must_use]
45 pub fn confirm_mutation(
46 argv: impl IntoIterator<Item = impl Into<OsString>>,
47 safety: SafetyClass,
48 ) -> Self {
49 Self::render_read_only(argv)
50 .with_mode(ExecutionMode::ConfirmMutation)
51 .with_safety(safety)
52 }
53
54 #[must_use]
56 pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
57 self.cwd = Some(cwd.into());
58 self
59 }
60
61 #[must_use]
63 pub fn with_repository(mut self, repository: impl Into<PathBuf>) -> Self {
64 self.global_options.repository = Some(repository.into());
65 self
66 }
67
68 #[must_use]
70 pub fn with_global_options(mut self, global_options: GlobalOptions) -> Self {
71 self.global_options = global_options;
72 self
73 }
74
75 #[must_use]
77 pub fn with_stdin(mut self, stdin: impl Into<String>) -> Self {
78 self.stdin = Some(stdin.into());
79 self
80 }
81
82 #[must_use]
84 pub fn with_title(mut self, title: impl Into<String>) -> Self {
85 self.title = title.into();
86 self
87 }
88
89 #[must_use]
91 pub const fn with_mode(mut self, mode: ExecutionMode) -> Self {
92 self.mode = mode;
93 self
94 }
95
96 #[must_use]
98 pub const fn with_safety(mut self, safety: SafetyClass) -> Self {
99 self.safety = safety;
100 self
101 }
102
103 #[must_use]
105 pub const fn with_refresh_plan(mut self, refresh_plan: RefreshPlan) -> Self {
106 self.refresh_plan = refresh_plan;
107 self
108 }
109
110 #[must_use]
112 pub fn argv(&self) -> &[OsString] {
113 &self.argv
114 }
115
116 #[must_use]
118 pub const fn global_options(&self) -> &GlobalOptions {
119 &self.global_options
120 }
121
122 #[must_use]
124 pub fn global_argv(&self) -> Vec<OsString> {
125 self.global_options.argv()
126 }
127
128 #[must_use]
130 pub fn process_argv(&self) -> Vec<OsString> {
131 let mut argv = self.global_argv();
132 argv.extend(self.argv.iter().cloned());
133 argv
134 }
135
136 #[must_use]
138 pub fn cwd(&self) -> Option<&Path> {
139 self.cwd.as_deref()
140 }
141
142 #[must_use]
144 pub fn repository(&self) -> Option<&Path> {
145 self.global_options.repository()
146 }
147
148 #[must_use]
150 pub fn stdin(&self) -> Option<&str> {
151 self.stdin.as_deref()
152 }
153
154 #[must_use]
156 pub fn title(&self) -> &str {
157 &self.title
158 }
159
160 #[must_use]
162 pub fn preview(&self) -> String {
163 preview_argv(&self.argv)
164 }
165
166 #[must_use]
168 pub fn process_preview(&self) -> String {
169 preview_argv(&self.process_argv())
170 }
171
172 #[must_use]
174 pub fn command_preview(&self) -> CommandPreview {
175 CommandPreview::from_spec(self)
176 }
177
178 #[must_use]
180 pub const fn mode(&self) -> ExecutionMode {
181 self.mode
182 }
183
184 #[must_use]
186 pub const fn safety(&self) -> SafetyClass {
187 self.safety
188 }
189
190 #[must_use]
192 pub const fn refresh_plan(&self) -> RefreshPlan {
193 self.refresh_plan
194 }
195}
196
197#[derive(Clone, Debug, Eq, PartialEq)]
199pub struct CommandPreview {
200 pub spec: JjCommandSpec,
202 pub title: String,
204 pub command_line: String,
206 pub execution_mode: ExecutionMode,
208 pub safety: SafetyClass,
210 pub refresh_plan: RefreshPlan,
212 pub warnings: Vec<CommandPreviewWarning>,
214}
215
216impl CommandPreview {
217 #[must_use]
219 pub fn from_spec(spec: &JjCommandSpec) -> Self {
220 Self {
221 spec: spec.clone(),
222 title: spec.title().to_owned(),
223 command_line: preview_argv(&redact_argv(spec.process_argv())),
224 execution_mode: spec.mode(),
225 safety: spec.safety(),
226 refresh_plan: spec.refresh_plan(),
227 warnings: CommandPreviewWarning::from_spec(spec),
228 }
229 }
230
231 #[must_use]
233 pub const fn requires_confirmation(&self) -> bool {
234 matches!(
235 self.execution_mode,
236 ExecutionMode::ConfirmMutation
237 | ExecutionMode::ConfirmExternalTool
238 | ExecutionMode::DryRunThenConfirm
239 )
240 }
241}
242
243#[derive(Clone, Debug, Eq, PartialEq)]
245#[non_exhaustive]
246pub enum CommandPreviewWarning {
247 LocalMetadata,
249 LocalRewrite,
251 DestructiveLocal,
253 NetworkWrite,
255 ExternalCommand,
257 IgnoresWorkingCopy,
259 AtOperation(String),
261 DoesNotIntegrateOperation,
263 IgnoresImmutableCommits,
265}
266
267impl CommandPreviewWarning {
268 fn from_spec(spec: &JjCommandSpec) -> Vec<Self> {
269 let mut warnings = Vec::new();
270 match spec.safety() {
271 SafetyClass::ReadOnly | SafetyClass::NetworkRead => {}
272 SafetyClass::LocalMetadata => warnings.push(Self::LocalMetadata),
273 SafetyClass::LocalRewrite => warnings.push(Self::LocalRewrite),
274 SafetyClass::DestructiveLocal => warnings.push(Self::DestructiveLocal),
275 SafetyClass::NetworkWrite => warnings.push(Self::NetworkWrite),
276 SafetyClass::ExternalCommand => warnings.push(Self::ExternalCommand),
277 }
278
279 match &spec.global_options.working_copy {
280 WorkingCopyPolicy::SnapshotAndUpdate => {}
281 WorkingCopyPolicy::Ignore => warnings.push(Self::IgnoresWorkingCopy),
282 }
283
284 match &spec.global_options.operation {
285 OperationLoadPolicy::Latest => {}
286 OperationLoadPolicy::AtOperation(operation) => {
287 warnings.push(Self::AtOperation(operation.clone()));
288 }
289 }
290
291 match spec.global_options.operation_integration {
292 OperationIntegrationPolicy::Integrate => {}
293 OperationIntegrationPolicy::DoNotIntegrate => {
294 warnings.push(Self::DoesNotIntegrateOperation);
295 }
296 }
297
298 match spec.global_options.immutability {
299 ImmutabilityPolicy::Enforce => {}
300 ImmutabilityPolicy::Ignore => warnings.push(Self::IgnoresImmutableCommits),
301 }
302
303 warnings
304 }
305}
306
307#[derive(Clone, Debug, Eq, PartialEq)]
309pub struct GlobalOptions {
310 repository: Option<PathBuf>,
311 working_copy: WorkingCopyPolicy,
312 operation: OperationLoadPolicy,
313 operation_integration: OperationIntegrationPolicy,
314 immutability: ImmutabilityPolicy,
315 output: OutputPolicy,
316 debug: bool,
317 config_overlays: Vec<ConfigOverlay>,
318}
319
320impl GlobalOptions {
321 #[must_use]
323 pub fn with_repository(mut self, repository: impl Into<PathBuf>) -> Self {
324 self.repository = Some(repository.into());
325 self
326 }
327
328 #[must_use]
330 pub const fn with_working_copy(mut self, working_copy: WorkingCopyPolicy) -> Self {
331 self.working_copy = working_copy;
332 self
333 }
334
335 #[must_use]
337 pub fn with_operation(mut self, operation: OperationLoadPolicy) -> Self {
338 self.operation = operation;
339 self
340 }
341
342 #[must_use]
344 pub const fn with_operation_integration(
345 mut self,
346 operation_integration: OperationIntegrationPolicy,
347 ) -> Self {
348 self.operation_integration = operation_integration;
349 self
350 }
351
352 #[must_use]
354 pub const fn with_immutability(mut self, immutability: ImmutabilityPolicy) -> Self {
355 self.immutability = immutability;
356 self
357 }
358
359 #[must_use]
361 pub const fn with_output(mut self, output: OutputPolicy) -> Self {
362 self.output = output;
363 self
364 }
365
366 #[must_use]
368 pub const fn with_debug(mut self, debug: bool) -> Self {
369 self.debug = debug;
370 self
371 }
372
373 #[must_use]
375 pub fn with_config_overlay(mut self, overlay: ConfigOverlay) -> Self {
376 self.config_overlays.push(overlay);
377 self
378 }
379
380 #[must_use]
382 pub fn repository(&self) -> Option<&Path> {
383 self.repository.as_deref()
384 }
385
386 #[must_use]
388 pub fn argv(&self) -> Vec<OsString> {
389 let mut argv = Vec::new();
390
391 self.output.push_argv(&mut argv);
392
393 if let Some(repository) = &self.repository {
394 argv.push("--repository".into());
395 argv.push(repository.as_os_str().to_owned());
396 }
397
398 match (&self.working_copy, &self.operation) {
399 (WorkingCopyPolicy::Ignore, OperationLoadPolicy::Latest) => {
400 argv.push("--ignore-working-copy".into());
401 }
402 (WorkingCopyPolicy::SnapshotAndUpdate, _)
403 | (WorkingCopyPolicy::Ignore, OperationLoadPolicy::AtOperation(_)) => {}
404 }
405
406 match &self.operation {
407 OperationLoadPolicy::Latest => {}
408 OperationLoadPolicy::AtOperation(operation) => {
409 argv.push("--at-operation".into());
410 argv.push(operation.as_str().into());
411 }
412 }
413
414 match self.operation_integration {
415 OperationIntegrationPolicy::Integrate => {}
416 OperationIntegrationPolicy::DoNotIntegrate => {
417 argv.push("--no-integrate-operation".into());
418 }
419 }
420
421 match self.immutability {
422 ImmutabilityPolicy::Enforce => {}
423 ImmutabilityPolicy::Ignore => argv.push("--ignore-immutable".into()),
424 }
425
426 if self.debug {
427 argv.push("--debug".into());
428 }
429
430 for overlay in &self.config_overlays {
431 overlay.push_argv(&mut argv);
432 }
433
434 argv
435 }
436}
437
438impl Default for GlobalOptions {
439 fn default() -> Self {
440 Self {
441 repository: None,
442 working_copy: WorkingCopyPolicy::SnapshotAndUpdate,
443 operation: OperationLoadPolicy::Latest,
444 operation_integration: OperationIntegrationPolicy::Integrate,
445 immutability: ImmutabilityPolicy::Enforce,
446 output: OutputPolicy::default(),
447 debug: false,
448 config_overlays: Vec::new(),
449 }
450 }
451}
452
453#[derive(Clone, Copy, Debug, Eq, PartialEq)]
455#[non_exhaustive]
456pub enum WorkingCopyPolicy {
457 SnapshotAndUpdate,
459 Ignore,
461}
462
463#[derive(Clone, Debug, Eq, PartialEq)]
465#[non_exhaustive]
466pub enum OperationLoadPolicy {
467 Latest,
469 AtOperation(String),
471}
472
473#[derive(Clone, Copy, Debug, Eq, PartialEq)]
475#[non_exhaustive]
476pub enum OperationIntegrationPolicy {
477 Integrate,
479 DoNotIntegrate,
481}
482
483#[derive(Clone, Copy, Debug, Eq, PartialEq)]
485#[non_exhaustive]
486pub enum ImmutabilityPolicy {
487 Enforce,
489 Ignore,
491}
492
493#[derive(Clone, Copy, Debug, Eq, PartialEq)]
495pub struct OutputPolicy {
496 pub color: ColorPolicy,
498 pub pager: PagerPolicy,
500 pub quiet: bool,
502}
503
504impl OutputPolicy {
505 fn push_argv(self, argv: &mut Vec<OsString>) {
506 match self.pager {
507 PagerPolicy::Disable => argv.push("--no-pager".into()),
508 PagerPolicy::Inherit => {}
509 }
510
511 argv.push("--color".into());
512 argv.push(self.color.as_str().into());
513
514 if self.quiet {
515 argv.push("--quiet".into());
516 }
517 }
518}
519
520impl Default for OutputPolicy {
521 fn default() -> Self {
522 Self {
523 color: ColorPolicy::Always,
524 pager: PagerPolicy::Disable,
525 quiet: false,
526 }
527 }
528}
529
530#[derive(Clone, Copy, Debug, Eq, PartialEq)]
532#[non_exhaustive]
533pub enum ColorPolicy {
534 Always,
536 Never,
538 Debug,
540 Auto,
542}
543
544impl ColorPolicy {
545 const fn as_str(self) -> &'static str {
546 match self {
547 Self::Always => "always",
548 Self::Never => "never",
549 Self::Debug => "debug",
550 Self::Auto => "auto",
551 }
552 }
553}
554
555#[derive(Clone, Copy, Debug, Eq, PartialEq)]
557#[non_exhaustive]
558pub enum PagerPolicy {
559 Disable,
561 Inherit,
563}
564
565#[derive(Clone, Debug, Eq, PartialEq)]
567#[non_exhaustive]
568pub enum ConfigOverlay {
569 Inline {
571 name_value: String,
573 },
574 File(PathBuf),
576}
577
578impl ConfigOverlay {
579 fn push_argv(&self, argv: &mut Vec<OsString>) {
580 match self {
581 Self::Inline { name_value } => {
582 argv.push("--config".into());
583 argv.push(name_value.as_str().into());
584 }
585 Self::File(path) => {
586 argv.push("--config-file".into());
587 argv.push(path.as_os_str().to_owned());
588 }
589 }
590 }
591}
592
593#[derive(Clone, Copy, Debug, Eq, PartialEq)]
595#[non_exhaustive]
596pub enum ExecutionMode {
597 RenderReadOnly,
599 ConfirmMutation,
601 ConfirmExternalTool,
603 DryRunThenConfirm,
605 CommandMode,
607}
608
609#[derive(Clone, Copy, Debug, Eq, PartialEq)]
611#[non_exhaustive]
612pub enum SafetyClass {
613 ReadOnly,
615 LocalMetadata,
617 LocalRewrite,
619 DestructiveLocal,
621 NetworkRead,
623 NetworkWrite,
625 ExternalCommand,
627}
628
629#[derive(Clone, Copy, Debug, Eq, PartialEq)]
631#[non_exhaustive]
632pub enum RefreshPlan {
633 None,
635 ReRunSpec,
637}
638
639pub fn preview_argv(argv: &[OsString]) -> String {
640 let mut preview = String::from("jj");
641 for arg in argv {
642 preview.push(' ');
643 preview.push_str("e_arg(arg));
644 }
645 preview
646}
647
648fn quote_arg(arg: &OsStr) -> String {
649 let arg = arg.to_string_lossy();
650 if arg.is_empty() {
651 return "''".to_owned();
652 }
653
654 if arg
655 .chars()
656 .all(|character| character.is_ascii_alphanumeric() || "-_./:@".contains(character))
657 {
658 return arg.into_owned();
659 }
660
661 format!("'{}'", arg.replace('\'', "'\"'\"'"))
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667
668 fn strings(argv: impl IntoIterator<Item = OsString>) -> Vec<String> {
669 argv.into_iter()
670 .map(|arg| arg.to_string_lossy().into_owned())
671 .collect()
672 }
673
674 #[test]
675 fn empty_argv_previews_as_jj() {
676 let spec = JjCommandSpec::render_read_only(Vec::<OsString>::new());
677
678 assert_eq!(spec.preview(), "jj");
679 assert_eq!(spec.title(), "jj");
680 }
681
682 #[test]
683 fn default_global_options_render_current_app_globals() {
684 let options = GlobalOptions::default();
685
686 assert_eq!(
687 strings(options.argv()),
688 vec!["--no-pager", "--color", "always"]
689 );
690 }
691
692 #[test]
693 fn repository_renders_before_command_argv() {
694 let spec =
695 JjCommandSpec::render_read_only(["diff", "-r", "@"]).with_repository("/tmp/repo");
696
697 assert_eq!(
698 strings(spec.process_argv()),
699 vec![
700 "--no-pager",
701 "--color",
702 "always",
703 "--repository",
704 "/tmp/repo",
705 "diff",
706 "-r",
707 "@"
708 ]
709 );
710 assert_eq!(spec.repository(), Some(Path::new("/tmp/repo")));
711 assert_eq!(
712 spec.global_options().repository(),
713 Some(Path::new("/tmp/repo"))
714 );
715 }
716
717 #[test]
718 fn advanced_global_options_render_before_command_argv() {
719 let global_options = GlobalOptions::default()
720 .with_operation(OperationLoadPolicy::AtOperation("abc123".to_owned()))
721 .with_operation_integration(OperationIntegrationPolicy::DoNotIntegrate)
722 .with_immutability(ImmutabilityPolicy::Ignore)
723 .with_debug(true);
724 let spec = JjCommandSpec::render_read_only(["status"]).with_global_options(global_options);
725
726 assert_eq!(
727 strings(spec.process_argv()),
728 vec![
729 "--no-pager",
730 "--color",
731 "always",
732 "--at-operation",
733 "abc123",
734 "--no-integrate-operation",
735 "--ignore-immutable",
736 "--debug",
737 "status"
738 ]
739 );
740 }
741
742 #[test]
743 fn ignore_working_copy_renders_before_command_argv() {
744 let global_options = GlobalOptions::default().with_working_copy(WorkingCopyPolicy::Ignore);
745 let spec = JjCommandSpec::render_read_only(["log"]).with_global_options(global_options);
746
747 assert_eq!(
748 strings(spec.process_argv()),
749 vec![
750 "--no-pager",
751 "--color",
752 "always",
753 "--ignore-working-copy",
754 "log"
755 ]
756 );
757 }
758
759 #[test]
760 fn at_operation_does_not_duplicate_implied_working_copy_ignore() {
761 let global_options = GlobalOptions::default()
762 .with_working_copy(WorkingCopyPolicy::Ignore)
763 .with_operation(OperationLoadPolicy::AtOperation("abc123".to_owned()));
764 let spec = JjCommandSpec::render_read_only(["status"]).with_global_options(global_options);
765
766 assert_eq!(
767 strings(spec.process_argv()),
768 vec![
769 "--no-pager",
770 "--color",
771 "always",
772 "--at-operation",
773 "abc123",
774 "status"
775 ]
776 );
777 }
778
779 #[test]
780 fn config_overlays_preserve_global_order() {
781 let options = GlobalOptions::default()
782 .with_config_overlay(ConfigOverlay::Inline {
783 name_value: "ui.color=always".to_owned(),
784 })
785 .with_config_overlay(ConfigOverlay::File("/tmp/jj.toml".into()))
786 .with_config_overlay(ConfigOverlay::Inline {
787 name_value: "aliases.l=log".to_owned(),
788 });
789
790 assert_eq!(
791 strings(options.argv()),
792 vec![
793 "--no-pager",
794 "--color",
795 "always",
796 "--config",
797 "ui.color=always",
798 "--config-file",
799 "/tmp/jj.toml",
800 "--config",
801 "aliases.l=log"
802 ]
803 );
804 }
805
806 #[test]
807 fn process_argv_does_not_duplicate_repository() {
808 let spec =
809 JjCommandSpec::render_read_only(["show", "@"]).with_repository("/tmp/repository");
810 let argv = strings(spec.process_argv());
811
812 assert_eq!(
813 argv.iter()
814 .filter(|arg| arg.as_str() == "--repository")
815 .count(),
816 1
817 );
818 }
819
820 #[test]
821 fn render_read_only_sets_default_policy() {
822 let spec = JjCommandSpec::render_read_only(["diff", "-r", "abc123"]);
823
824 assert_eq!(spec.mode(), ExecutionMode::RenderReadOnly);
825 assert_eq!(spec.safety(), SafetyClass::ReadOnly);
826 assert_eq!(spec.refresh_plan(), RefreshPlan::ReRunSpec);
827 }
828
829 #[test]
830 fn preview_quotes_whitespace_and_shell_metacharacters() {
831 let spec = JjCommandSpec::render_read_only(["log", "-r", "description('a b')"]);
832
833 assert_eq!(spec.preview(), "jj log -r 'description('\"'\"'a b'\"'\"')'");
834 }
835
836 #[test]
837 fn preview_quotes_backticks() {
838 let spec = JjCommandSpec::render_read_only(["log", "-r", "`echo nope`"]);
839
840 assert_eq!(spec.preview(), "jj log -r '`echo nope`'");
841 }
842
843 #[test]
844 fn process_preview_includes_global_options_before_command_argv() {
845 let spec =
846 JjCommandSpec::render_read_only(["status"]).with_repository("/tmp/repo with spaces");
847
848 assert_eq!(
849 spec.process_preview(),
850 "jj --no-pager --color always --repository '/tmp/repo with spaces' status"
851 );
852 assert_eq!(spec.preview(), "jj status");
853 }
854
855 #[test]
856 fn confirm_mutation_sets_preview_policy() {
857 let spec = JjCommandSpec::confirm_mutation(
858 ["workspace", "update-stale"],
859 SafetyClass::LocalMetadata,
860 );
861
862 assert_eq!(spec.mode(), ExecutionMode::ConfirmMutation);
863 assert_eq!(spec.safety(), SafetyClass::LocalMetadata);
864 assert_eq!(spec.refresh_plan(), RefreshPlan::ReRunSpec);
865 }
866
867 #[test]
868 fn command_preview_uses_process_command_line_and_short_title() {
869 let global_options = GlobalOptions::default()
870 .with_repository("/tmp/repo")
871 .with_working_copy(WorkingCopyPolicy::Ignore);
872 let spec = JjCommandSpec::confirm_mutation(["new", "@"], SafetyClass::LocalRewrite)
873 .with_global_options(global_options)
874 .with_title("new change after @");
875
876 let preview = spec.command_preview();
877
878 assert_eq!(preview.spec, spec);
879 assert_eq!(preview.title, "new change after @");
880 assert_eq!(
881 preview.command_line,
882 "jj --no-pager --color always --repository /tmp/repo --ignore-working-copy new @"
883 );
884 assert_eq!(preview.execution_mode, ExecutionMode::ConfirmMutation);
885 assert_eq!(preview.safety, SafetyClass::LocalRewrite);
886 assert_eq!(preview.refresh_plan, RefreshPlan::ReRunSpec);
887 assert!(preview.requires_confirmation());
888 assert_eq!(
889 preview.warnings,
890 vec![
891 CommandPreviewWarning::LocalRewrite,
892 CommandPreviewWarning::IgnoresWorkingCopy
893 ]
894 );
895 }
896
897 #[test]
898 fn command_preview_redacts_secret_looking_global_options() {
899 let global_options = GlobalOptions::default().with_config_overlay(ConfigOverlay::Inline {
900 name_value: "auth.token=abc123".to_owned(),
901 });
902 let spec = JjCommandSpec::confirm_mutation(
903 ["describe", "-m", "safe", "@"],
904 SafetyClass::LocalRewrite,
905 )
906 .with_global_options(global_options);
907
908 let preview = spec.command_preview();
909
910 assert_eq!(
911 preview.command_line,
912 "jj --no-pager --color always --config 'auth.token=<redacted>' describe -m safe @"
913 );
914 assert_eq!(preview.spec.process_argv(), spec.process_argv());
915 }
916
917 #[test]
918 fn command_preview_warns_for_advanced_global_safety_flags() {
919 let global_options = GlobalOptions::default()
920 .with_operation(OperationLoadPolicy::AtOperation("abc123".to_owned()))
921 .with_operation_integration(OperationIntegrationPolicy::DoNotIntegrate)
922 .with_immutability(ImmutabilityPolicy::Ignore);
923 let spec = JjCommandSpec::confirm_mutation(
924 ["rebase", "-b", "@", "-d", "main"],
925 SafetyClass::LocalRewrite,
926 )
927 .with_global_options(global_options);
928
929 let preview = spec.command_preview();
930
931 assert_eq!(
932 preview.warnings,
933 vec![
934 CommandPreviewWarning::LocalRewrite,
935 CommandPreviewWarning::AtOperation("abc123".to_owned()),
936 CommandPreviewWarning::DoesNotIntegrateOperation,
937 CommandPreviewWarning::IgnoresImmutableCommits
938 ]
939 );
940 }
941
942 #[test]
943 fn metadata_builders_preserve_argv() {
944 let spec = JjCommandSpec::render_read_only(["diff"])
945 .with_cwd("/tmp/work")
946 .with_repository("/tmp/repo")
947 .with_stdin("input")
948 .with_title("custom");
949
950 assert_eq!(spec.argv(), &[OsString::from("diff")]);
951 assert_eq!(spec.cwd(), Some(Path::new("/tmp/work")));
952 assert_eq!(spec.repository(), Some(Path::new("/tmp/repo")));
953 assert_eq!(spec.stdin(), Some("input"));
954 assert_eq!(spec.title(), "custom");
955 assert_eq!(spec.preview(), "jj diff");
956 }
957}