just playing with tangled

cli squash: new --restore-descendants option #2

open opened by ilyagr.bsky.social targeting main from squash-no-restore

This option is already implemented for abandon, restore, diffedit.

Includes GHC-style note explaining why a single option for restoring both source and target descendants is sufficient and simplest to understand in corner cases. Currently, this note is only relevant to squash.rs, but it would also be referenced in jj rebase and jj duplicate once those gain --restore-descendants.

Fixes #6000

Labels

None yet.

Participants 1
AT URI
at://did:plc:jp6rly3c67o3zlwarw2ttafu/sh.tangled.repo.pull/3lqh3irrzaz22
+968 -13
Diff #0
+3
CHANGELOG.md
··· 324 324 * The 'how to resolve conflicts' hint that is shown when conflicts appear can 325 325 be hidden by setting `hints.resolving-conflicts = false`. 326 326 327 + * `jj squash` now has a `--restore-descendants` option to preserve the snapshots 328 + of the children of the modified commits. 329 + 327 330 * `jj op diff` and `jj op log --op-diff` now show changes to which commits 328 331 correspond to working copies. 329 332
+156 -2
cli/src/commands/squash.rs
··· 22 22 use jj_lib::repo::Repo as _; 23 23 use jj_lib::rewrite; 24 24 use jj_lib::rewrite::CommitWithSelection; 25 + use jj_lib::rewrite::SquashOptions; 25 26 use tracing::instrument; 26 27 27 28 use crate::cli_util::CommandHelper; ··· 112 113 /// The source revision will not be abandoned 113 114 #[arg(long, short)] 114 115 keep_emptied: bool, 116 + /// Preserve the content (not the diff) when rebasing descendants of the 117 + /// source and target commits 118 + /// 119 + /// Only the snapshots of the `--from` and the `--into` commits will be 120 + /// modified. 121 + /// 122 + /// If you'd like to preserve the content of *only* the target's descendants 123 + /// (or *only* the source's), consider using `jj rebase -r` or `jj 124 + /// duplicate` before squashing. 125 + // 126 + // See "NOTE: Not implementing `--restore-{target,source}-descendants`" in 127 + // squash.rs. 128 + // 129 + // TODO: Once it's implemented, we should recommend `jj rebase -r 130 + // --restore-descendants` instead of `jj duplicate`, since you actually 131 + // would need to `squash` twice with `duplicate`. 132 + #[arg(long)] 133 + restore_descendants: bool, 115 134 } 116 135 136 + // NOTE: Not implementing `--restore-{target,source}-descendants` 137 + // -------------------------------------------------------------- 138 + // 139 + // We have `jj squash --restore-descendants --from X --into Y` preserve the 140 + // snapshots of both the descendants of `X` and those of the descendants of `Y`. 141 + // This behavior makes it simple to understand; it does the same thing to the 142 + // child of any commit `jj squash` rewrites. As @yuja pointed out it could even 143 + // be a global flag that would apply to any command that rewrites commits. 144 + // 145 + // In this note, we explain why we choose not to have a flag for `jj squash` 146 + // that preserves *only* the descendants of the source (call it 147 + // `--restore-source-descendants`) or a similar `--restore-target-descendants` 148 + // flag, even though they might seem easy to implement at a glance. 149 + // 150 + // (The same argument applies to `jj rebase --restore-???-descendants`.) 151 + // 152 + // Firstly, such extra flags seem to only be useful in rare cases. If needed, 153 + // they can be simulated. Instead of `squash --restore-target-descendants`, you 154 + // could do `jj rebase -r X -d all:X-; jj squash --restore-descendants --from X 155 + // --into Y`. Instead of `squash --restore-source-descendants`, you could do `jj 156 + // duplicate -r X; jj squash --restore-descendants --from copy_of_X --into Y; jj 157 + // abandon --restore-descendants X`. (TODO: When `jj rebase -r 158 + // --restore-descendants` is implemented, this will become 2 commands instead of 159 + // 3). 160 + // 161 + // Secondly, the behavior of these flags would get confusing in corner cases, 162 + // when the target is an ancestor or descendant of the source, or for ancestors 163 + // of merge commits. For example, consider this commit graph with merge commit 164 + // `Z` where `A` is *not* empty (thanks to @lilyball for suggesting the merge 165 + // commit example): 166 + // 167 + // ``` 168 + // A -> X - 169 + // \ (Example I) 170 + // B -> Y --->Z 171 + // ``` 172 + // 173 + // The behavior of `jj squash --from A --into B --restore-descendants` is easy 174 + // to understand: the snapshots of `X` and `Y` remain the same, and all of their 175 + // descendants also remain the same by normal rebasing rules. 176 + // 177 + // If we allowed `jj squash --from A --into B --restore-target-descendants`, 178 + // what should it mean? It seems clear that `X`'s snapshot should remain the 179 + // same, and `X`'s will change. However, should `Z`'s snapshot change? If we 180 + // follow the logic that Z had one of its parents change and the other stay the 181 + // same, it seems that yes, it should. This is also what the equivalence with 182 + // `jj rebase -r A -d A-; jj squash --from A --into B --restore-descendants` 183 + // would imply. 184 + // 185 + // (A contrarian mind could argue that `Z`'s snapshot should be preserved since 186 + // `Z` is a descendant of the target `B`. We'll put this thought aside for a 187 + // moment and keep going, to see how things get even more confusing.) 188 + // 189 + // Now, let's pretend we squashed `X` and `Y` into `Z` and ask the same 190 + // question. Our graph is now: 191 + // 192 + // ``` 193 + // A - 194 + // \ (Example II) 195 + // B --->Z 196 + // ``` 197 + // 198 + // By the logic above, the snapshot of `Z` will again change after `jj squash 199 + // --from A --into B --restore-target-descendants`. This is unsatisfying and 200 + // would probably be unexpected, since `Z` is a direct child of the target 201 + // commit `B`, so the user might expect its snapshot to be preserved. 202 + // 203 + // Now, there are a few options: 204 + // 205 + // 1. Allow the confusing but seemingly correct definition of 206 + // `--restore-target-descendants` as above. 207 + // 2. Allow `--restore-target-descendants`, but forbid it in some set of 208 + // situations we deem too confusing. 209 + // 3. Have the effect of `jj squash --from A --into B 210 + // --restore-target-descendants` on `Z`'s snapshot differ between Example I 211 + // and Example II. In other words, the behavior will depend on whether there 212 + // are commits (even if they are empty commits!) between `A` and `Z`, or 213 + // between `B` and `Z`. 214 + // 4. Declare that in both Example I and Example II above, the snapshot of `Z` 215 + // should be preserved. 216 + // 217 + // The first problem with this (and with option 3 above) would be that 218 + // `--restore-target-descendants` would now be equivalent to a rebase 219 + // followed by `squash --restore-descendants` *almost* always, but would 220 + // differ in corner cases. 221 + // 222 + // Perhaps more importantly, this would break the important property of `jj 223 + // squash --restore-target-descendants` that its difference from the 224 + // behavior of normal `jj squash` is local; affects only the direct children 225 + // of the modified commits. All others can normally be rebased by normal 226 + // `jj` rules. 227 + // 228 + // If `jj squash --restore-target-descendants` preserved the snapshot of `Z` 229 + // even if there are 100 commit between it and `A`, this would change its 230 + // diff relative to its parents, possibly without any awareness from the 231 + // user that this happened or that `Z` even existed. 232 + // 5. Do not provide `--restore-target-descendants` ourselves, and recommend 233 + // that the user manually does `jj rebase -r X -d all:X-; jj squash 234 + // --restore-descendants --from X --into Y` if they really need it. 235 + // 236 + // The last option seems easiest. It also has the advantage of requiring fewer 237 + // tests and being the simplest to maintain. 238 + // 239 + // Aside: the merge example is probably the easiest to understand and the most 240 + // problematic, but for `X -> A -> B -> C -> D`, both `jj squash --from C 241 + // --into A --restore-target-descendants` and `jj squash --from A --into C 242 + // --restore-source-descendants` have similar problems. 243 + 117 244 #[instrument(skip_all)] 118 245 pub(crate) fn cmd_squash( 119 246 ui: &mut Ui, ··· 166 293 .check_rewritable(sources.iter().chain(std::iter::once(&destination)).ids())?; 167 294 168 295 let mut tx = workspace_command.start_transaction(); 169 - let tx_description = format!("squash commits into {}", destination.id().hex()); 296 + let tx_description = format!( 297 + "squash commits into {}{}", 298 + destination.id().hex(), 299 + if args.restore_descendants { 300 + " while preserving descendant contents" 301 + } else { 302 + "" 303 + } 304 + ); 170 305 let source_commits = select_diff(&tx, &sources, &destination, &matcher, &diff_selector)?; 171 306 if let Some(squashed) = rewrite::squash_commits( 172 307 tx.repo_mut(), 173 308 &source_commits, 174 309 &destination, 175 - args.keep_emptied, 310 + SquashOptions { 311 + keep_emptied: args.keep_emptied, 312 + // See "NOTE: Not implementing `--restore-{target,source}-descendants`" in 313 + // squash.rs. 314 + restore_descendants: args.restore_descendants, 315 + }, 176 316 )? { 177 317 let mut commit_builder = squashed.commit_builder.detach(); 178 318 let new_description = match description { ··· 220 360 }; 221 361 commit_builder.set_description(new_description); 222 362 commit_builder.write(tx.repo_mut())?; 363 + 364 + if args.restore_descendants { 365 + // If !args.restore_descendants, the corresponding steps are done inside 366 + // tx.finish() 367 + let num_reparented = tx.repo_mut().reparent_descendants()?; 368 + if let Some(mut formatter) = ui.status_formatter() { 369 + writeln!( 370 + formatter, 371 + "Rebased {num_reparented} descendant commits (while preserving their content)", 372 + )?; 373 + } 374 + } 223 375 } else { 224 376 if diff_selector.is_interactive() { 225 377 return Err(user_error("No changes selected")); ··· 241 393 } 242 394 } 243 395 } 396 + // TODO: Show the "Rebase NNN descendant commits message", add " (while 397 + // preserving their content)" in the --restore-descendants mode 244 398 tx.finish(ui, tx_description)?; 245 399 Ok(()) 246 400 }
+5
cli/tests/cli-reference@.md.snap
··· 2524 2524 * `-i`, `--interactive` โ€” Interactively choose which parts to squash 2525 2525 * `--tool <NAME>` โ€” Specify diff editor to be used (implies --interactive) 2526 2526 * `-k`, `--keep-emptied` โ€” The source revision will not be abandoned 2527 + * `--restore-descendants` โ€” Preserve the content (not the diff) when rebasing descendants of the source and target commits 2528 + 2529 + Only the snapshots of the `--from` and the `--into` commits will be modified. 2530 + 2531 + If you'd like to preserve the content of *only* the target's descendants (or *only* the source's), consider using `jj rebase -r` or `jj duplicate` before squashing. 2527 2532 2528 2533 2529 2534
+775
cli/tests/test_squash_command.rs
··· 745 745 "); 746 746 } 747 747 748 + #[test] 749 + fn test_squash_working_copy_restore_descendants() { 750 + let test_env = TestEnvironment::default(); 751 + test_env.run_jj_in(".", ["git", "init", "repo"]).success(); 752 + let work_dir = test_env.work_dir("repo"); 753 + 754 + // Create history like this: 755 + // Y 756 + // | 757 + // B X@ 758 + // |/ 759 + // A 760 + // 761 + // Each commit adds a file named the same as the commit 762 + let create_commit = |name: &str| { 763 + work_dir 764 + .run_jj(["bookmark", "create", "-r@", name]) 765 + .success(); 766 + work_dir.write_file(name, format!("test {name}\n")); 767 + }; 768 + 769 + create_commit("a"); 770 + work_dir.run_jj(["new"]).success(); 771 + create_commit("b"); 772 + work_dir.run_jj(["new", "a"]).success(); 773 + create_commit("x"); 774 + work_dir.run_jj(["new"]).success(); 775 + create_commit("y"); 776 + work_dir.run_jj(["edit", "x"]).success(); 777 + 778 + let template = r#"separate( 779 + " ", 780 + commit_id.short(), 781 + bookmarks, 782 + description, 783 + if(empty, "(empty)") 784 + )"#; 785 + let run_log = || work_dir.run_jj(["log", "-r=::", "--summary", "-T", template]); 786 + 787 + // Verify the setup 788 + insta::assert_snapshot!(run_log(), @r" 789 + โ—‹ 3f45d7a3ae69 y 790 + โ”‚ A y 791 + @ 5b4046443e64 x 792 + โ”‚ A x 793 + โ”‚ โ—‹ b1e1eea2f666 b 794 + โ”œโ”€โ•ฏ A b 795 + โ—‹ 7468364c89fc a 796 + โ”‚ A a 797 + โ—† 000000000000 (empty) 798 + [EOF] 799 + "); 800 + let output = work_dir.run_jj(["file", "list", "-r=a"]); 801 + insta::assert_snapshot!(output, @r" 802 + a 803 + [EOF] 804 + "); 805 + let output = work_dir.run_jj(["file", "list", "-r=b"]); 806 + insta::assert_snapshot!(output, @r" 807 + a 808 + b 809 + [EOF] 810 + "); 811 + let output = work_dir.run_jj(["file", "list"]); 812 + insta::assert_snapshot!(output, @r" 813 + a 814 + x 815 + [EOF] 816 + "); 817 + let output = work_dir.run_jj(["file", "list", "-r=y"]); 818 + insta::assert_snapshot!(output, @r" 819 + a 820 + x 821 + y 822 + [EOF] 823 + "); 824 + 825 + let output = work_dir.run_jj(["squash", "--restore-descendants"]); 826 + insta::assert_snapshot!(output, @r" 827 + ------- stderr ------- 828 + Rebased 2 descendant commits (while preserving their content) 829 + Working copy (@) now at: kxryzmor 7ec5499d (empty) (no description set) 830 + Parent commit (@-) : qpvuntsm 1c6a069e a x | (no description set) 831 + [EOF] 832 + "); 833 + insta::assert_snapshot!(run_log(), @r" 834 + @ 7ec5499d9141 (empty) 835 + โ”‚ โ—‹ ddfef0b279f8 y 836 + โ”œโ”€โ•ฏ A y 837 + โ”‚ โ—‹ 640ba5e85507 b 838 + โ”œโ”€โ•ฏ A b 839 + โ”‚ D x 840 + โ—‹ 1c6a069ec7e3 a x 841 + โ”‚ A a 842 + โ”‚ A x 843 + โ—† 000000000000 (empty) 844 + [EOF] 845 + "); 846 + 847 + let output = work_dir.run_jj(["diff", "--summary"]); 848 + // The current commit becomes empty. 849 + insta::assert_snapshot!(output, @""); 850 + // Should coincide with the working copy commit before 851 + let output = work_dir.run_jj(["file", "list", "-r=a"]); 852 + insta::assert_snapshot!(output, @r" 853 + a 854 + x 855 + [EOF] 856 + "); 857 + // Commit b should be the same as before 858 + let output = work_dir.run_jj(["file", "list", "-r=b"]); 859 + insta::assert_snapshot!(output, @r" 860 + a 861 + b 862 + [EOF] 863 + "); 864 + let output = work_dir.run_jj(["file", "list", "-r=y"]); 865 + insta::assert_snapshot!(output, @r" 866 + a 867 + x 868 + y 869 + [EOF] 870 + "); 871 + } 872 + 873 + #[test] 874 + fn test_squash_from_to_restore_descendants() { 875 + let test_env = TestEnvironment::default(); 876 + test_env.run_jj_in(".", ["git", "init", "repo"]).success(); 877 + let work_dir = test_env.work_dir("repo"); 878 + 879 + // Create history like this: 880 + // F 881 + // |\ 882 + // E C 883 + // | | 884 + // D B 885 + // |/ 886 + // A 887 + // 888 + // Each commit adds a file named the same as the commit 889 + let create_commit = |name: &str| { 890 + work_dir 891 + .run_jj(["bookmark", "create", "-r@", name]) 892 + .success(); 893 + work_dir.write_file(name, format!("test {name}\n")); 894 + }; 895 + 896 + create_commit("a"); 897 + work_dir.run_jj(["new"]).success(); 898 + create_commit("b"); 899 + work_dir.run_jj(["new"]).success(); 900 + create_commit("c"); 901 + work_dir.run_jj(["new", "a"]).success(); 902 + create_commit("d"); 903 + work_dir.run_jj(["new"]).success(); 904 + create_commit("e"); 905 + work_dir.run_jj(["new", "e", "c"]).success(); 906 + create_commit("f"); 907 + 908 + let template = r#"separate( 909 + " ", 910 + commit_id.short(), 911 + bookmarks, 912 + description, 913 + if(empty, "(empty)") 914 + )"#; 915 + let run_log = || work_dir.run_jj(["log", "-r=::", "--summary", "-T", template]); 916 + 917 + // ========== Part 1 ========= 918 + // Verify the setup 919 + insta::assert_snapshot!(run_log(), @r" 920 + @ 42acd0537c88 f 921 + โ”œโ”€โ•ฎ A f 922 + โ”‚ โ—‹ 4fb9706b0f47 c 923 + โ”‚ โ”‚ A c 924 + โ”‚ โ—‹ b1e1eea2f666 b 925 + โ”‚ โ”‚ A b 926 + โ—‹ โ”‚ b4e3197108ba e 927 + โ”‚ โ”‚ A e 928 + โ—‹ โ”‚ d707102f499f d 929 + โ”œโ”€โ•ฏ A d 930 + โ—‹ 7468364c89fc a 931 + โ”‚ A a 932 + โ—† 000000000000 (empty) 933 + [EOF] 934 + "); 935 + let beginning = work_dir.current_operation_id(); 936 + test_env.advance_test_rng_seed_to_multiple_of(200_000); 937 + 938 + // Squash without --restore-descendants for comparison 939 + work_dir 940 + .run_jj(["operation", "restore", &beginning]) 941 + .success(); 942 + let output = work_dir.run_jj(["squash", "--from=b", "--into=d"]); 943 + insta::assert_snapshot!(output, @r" 944 + ------- stderr ------- 945 + Rebased 3 descendant commits 946 + Working copy (@) now at: kpqxywon e462100a f | (no description set) 947 + Parent commit (@-) : yostqsxw 6944fd03 e | (no description set) 948 + Parent commit (@-) : mzvwutvl 6cd5d5c1 c | (no description set) 949 + [EOF] 950 + "); 951 + insta::assert_snapshot!(run_log(), @r" 952 + @ e462100ae7c3 f 953 + โ”œโ”€โ•ฎ A f 954 + โ”‚ โ—‹ 6cd5d5c1daf7 c 955 + โ”‚ โ”‚ A c 956 + โ—‹ โ”‚ 6944fd03dc5d e 957 + โ”‚ โ”‚ A e 958 + โ—‹ โ”‚ 1befcf027d1b d 959 + โ”œโ”€โ•ฏ A b 960 + โ”‚ A d 961 + โ—‹ 7468364c89fc a b 962 + โ”‚ A a 963 + โ—† 000000000000 (empty) 964 + [EOF] 965 + "); 966 + let output = work_dir.run_jj(["file", "list", "-r=d"]); 967 + insta::assert_snapshot!(output, @r" 968 + a 969 + b 970 + d 971 + [EOF] 972 + "); 973 + let output = work_dir.run_jj(["file", "list", "-r=c"]); 974 + insta::assert_snapshot!(output, @r" 975 + a 976 + c 977 + [EOF] 978 + "); 979 + let output = work_dir.run_jj(["file", "list", "-r=e"]); 980 + insta::assert_snapshot!(output, @r" 981 + a 982 + b 983 + d 984 + e 985 + [EOF] 986 + "); 987 + let output = work_dir.run_jj(["file", "list", "-r=f"]); 988 + insta::assert_snapshot!(output, @r" 989 + a 990 + b 991 + c 992 + d 993 + e 994 + f 995 + [EOF] 996 + "); 997 + 998 + // --restore-descendants 999 + work_dir 1000 + .run_jj(["operation", "restore", &beginning]) 1001 + .success(); 1002 + let output = work_dir.run_jj(["squash", "--from=b", "--into=d", "--restore-descendants"]); 1003 + insta::assert_snapshot!(output, @r" 1004 + ------- stderr ------- 1005 + Rebased 3 descendant commits (while preserving their content) 1006 + Working copy (@) now at: kpqxywon 1d64ccbf f | (no description set) 1007 + Parent commit (@-) : yostqsxw cb90d752 e | (no description set) 1008 + Parent commit (@-) : mzvwutvl 4e6702ae c | (no description set) 1009 + [EOF] 1010 + "); 1011 + // `d`` becomes the same as in the above example, 1012 + // but `c` does not lose file `b` and `e` still does not contain file `b` 1013 + // regardless of what happened to their parents. 1014 + insta::assert_snapshot!(run_log(), @r" 1015 + @ 1d64ccbf4608 f 1016 + โ”œโ”€โ•ฎ A f 1017 + โ”‚ โ—‹ 4e6702ae494c c 1018 + โ”‚ โ”‚ A b 1019 + โ”‚ โ”‚ A c 1020 + โ—‹ โ”‚ cb90d75271b4 e 1021 + โ”‚ โ”‚ D b 1022 + โ”‚ โ”‚ A e 1023 + โ—‹ โ”‚ 853ea07451aa d 1024 + โ”œโ”€โ•ฏ A b 1025 + โ”‚ A d 1026 + โ—‹ 7468364c89fc a b 1027 + โ”‚ A a 1028 + โ—† 000000000000 (empty) 1029 + [EOF] 1030 + "); 1031 + let output = work_dir.run_jj(["file", "list", "-r=d"]); 1032 + insta::assert_snapshot!(output, @r" 1033 + a 1034 + b 1035 + d 1036 + [EOF] 1037 + "); 1038 + let output = work_dir.run_jj(["file", "list", "-r=c"]); 1039 + insta::assert_snapshot!(output, @r" 1040 + a 1041 + b 1042 + c 1043 + [EOF] 1044 + "); 1045 + let output = work_dir.run_jj(["file", "list", "-r=e"]); 1046 + insta::assert_snapshot!(output, @r" 1047 + a 1048 + d 1049 + e 1050 + [EOF] 1051 + "); 1052 + let output = work_dir.run_jj(["file", "list", "-r=f"]); 1053 + insta::assert_snapshot!(output, @r" 1054 + a 1055 + b 1056 + c 1057 + d 1058 + e 1059 + f 1060 + [EOF] 1061 + "); 1062 + 1063 + // --restore-descendants works with --keep-emptied, same result except for 1064 + // leaving an empty commit 1065 + work_dir 1066 + .run_jj(["operation", "restore", &beginning]) 1067 + .success(); 1068 + let output = work_dir.run_jj([ 1069 + "squash", 1070 + "--from=b", 1071 + "--into=d", 1072 + "--restore-descendants", 1073 + "--keep-emptied", 1074 + ]); 1075 + insta::assert_snapshot!(output, @r" 1076 + ------- stderr ------- 1077 + Rebased 3 descendant commits (while preserving their content) 1078 + Working copy (@) now at: kpqxywon 3c13920f f | (no description set) 1079 + Parent commit (@-) : yostqsxw aa73012d e | (no description set) 1080 + Parent commit (@-) : mzvwutvl d323deaa c | (no description set) 1081 + [EOF] 1082 + "); 1083 + // `d`` becomes the same as in the above example, 1084 + // but `c` does not lose file `b` and `e` still does not contain file `b` 1085 + // regardless of what happened to their parents. 1086 + insta::assert_snapshot!(run_log(), @r" 1087 + @ 3c13920f1e9a f 1088 + โ”œโ”€โ•ฎ A f 1089 + โ”‚ โ—‹ d323deaa04c2 c 1090 + โ”‚ โ”‚ A b 1091 + โ”‚ โ”‚ A c 1092 + โ”‚ โ—‹ a55451e8808f b (empty) 1093 + โ—‹ โ”‚ aa73012df9cd e 1094 + โ”‚ โ”‚ D b 1095 + โ”‚ โ”‚ A e 1096 + โ—‹ โ”‚ d00e73142243 d 1097 + โ”œโ”€โ•ฏ A b 1098 + โ”‚ A d 1099 + โ—‹ 7468364c89fc a 1100 + โ”‚ A a 1101 + โ—† 000000000000 (empty) 1102 + [EOF] 1103 + "); 1104 + let output = work_dir.run_jj(["file", "list", "-r=d"]); 1105 + insta::assert_snapshot!(output, @r" 1106 + a 1107 + b 1108 + d 1109 + [EOF] 1110 + "); 1111 + let output = work_dir.run_jj(["file", "list", "-r=c"]); 1112 + insta::assert_snapshot!(output, @r" 1113 + a 1114 + b 1115 + c 1116 + [EOF] 1117 + "); 1118 + let output = work_dir.run_jj(["file", "list", "-r=e"]); 1119 + insta::assert_snapshot!(output, @r" 1120 + a 1121 + d 1122 + e 1123 + [EOF] 1124 + "); 1125 + 1126 + // ========== Part 2 ========= 1127 + // Reminder of the setup 1128 + test_env.advance_test_rng_seed_to_multiple_of(200_000); 1129 + work_dir 1130 + .run_jj(["operation", "restore", &beginning]) 1131 + .success(); 1132 + insta::assert_snapshot!(run_log(), @r" 1133 + @ 42acd0537c88 f 1134 + โ”œโ”€โ•ฎ A f 1135 + โ”‚ โ—‹ 4fb9706b0f47 c 1136 + โ”‚ โ”‚ A c 1137 + โ”‚ โ—‹ b1e1eea2f666 b 1138 + โ”‚ โ”‚ A b 1139 + โ—‹ โ”‚ b4e3197108ba e 1140 + โ”‚ โ”‚ A e 1141 + โ—‹ โ”‚ d707102f499f d 1142 + โ”œโ”€โ•ฏ A d 1143 + โ—‹ 7468364c89fc a 1144 + โ”‚ A a 1145 + โ—† 000000000000 (empty) 1146 + [EOF] 1147 + "); 1148 + let output = work_dir.run_jj(["file", "list", "-r=c"]); 1149 + insta::assert_snapshot!(output, @r" 1150 + a 1151 + b 1152 + c 1153 + [EOF] 1154 + "); 1155 + let output = work_dir.run_jj(["file", "list", "-r=d"]); 1156 + insta::assert_snapshot!(output, @r" 1157 + a 1158 + d 1159 + [EOF] 1160 + "); 1161 + 1162 + // --restore-descendants works when squashing from parent to child 1163 + work_dir 1164 + .run_jj(["operation", "restore", &beginning]) 1165 + .success(); 1166 + let output = work_dir.run_jj(["squash", "--from=a", "--into=b", "--restore-descendants"]); 1167 + insta::assert_snapshot!(output, @r" 1168 + ------- stderr ------- 1169 + Rebased 2 descendant commits (while preserving their content) 1170 + Working copy (@) now at: kpqxywon 7fa445c9 f | (no description set) 1171 + Parent commit (@-) : yostqsxw 102e6106 e | (no description set) 1172 + Parent commit (@-) : mzvwutvl a2ff7c27 c | (no description set) 1173 + [EOF] 1174 + "); 1175 + insta::assert_snapshot!(run_log(), @r" 1176 + @ 7fa445c9e606 f 1177 + โ”œโ”€โ•ฎ A f 1178 + โ”‚ โ—‹ a2ff7c27dbba c 1179 + โ”‚ โ”‚ A c 1180 + โ”‚ โ—‹ 2bf81678391c b 1181 + โ”‚ โ”‚ A a 1182 + โ”‚ โ”‚ A b 1183 + โ—‹ โ”‚ 102e61065eb2 e 1184 + โ”‚ โ”‚ A e 1185 + โ—‹ โ”‚ 7b1493a2027e d 1186 + โ”œโ”€โ•ฏ A a 1187 + โ”‚ A d 1188 + โ—† 000000000000 a (empty) 1189 + [EOF] 1190 + "); 1191 + let output = work_dir.run_jj(["file", "list", "-r=b"]); 1192 + insta::assert_snapshot!(output, @r" 1193 + a 1194 + b 1195 + [EOF] 1196 + "); 1197 + let output = work_dir.run_jj(["file", "list", "-r=c"]); 1198 + insta::assert_snapshot!(output, @r" 1199 + a 1200 + b 1201 + c 1202 + [EOF] 1203 + "); 1204 + let output = work_dir.run_jj(["file", "list", "-r=d"]); 1205 + insta::assert_snapshot!(output, @r" 1206 + a 1207 + d 1208 + [EOF] 1209 + "); 1210 + 1211 + // --restore-descendants --keep-emptied works when squashing from parent to 1212 + // child 1213 + work_dir 1214 + .run_jj(["operation", "restore", &beginning]) 1215 + .success(); 1216 + let output = work_dir.run_jj([ 1217 + "squash", 1218 + "--from=a", 1219 + "--into=b", 1220 + "--restore-descendants", 1221 + "--keep-emptied", 1222 + ]); 1223 + insta::assert_snapshot!(output, @r" 1224 + ------- stderr ------- 1225 + Rebased 2 descendant commits (while preserving their content) 1226 + Working copy (@) now at: kpqxywon 30c1ec1b f | (no description set) 1227 + Parent commit (@-) : yostqsxw c20a2a7a e | (no description set) 1228 + Parent commit (@-) : mzvwutvl 601223f5 c | (no description set) 1229 + [EOF] 1230 + "); 1231 + insta::assert_snapshot!(run_log(), @r" 1232 + @ 30c1ec1b6264 f 1233 + โ”œโ”€โ•ฎ A f 1234 + โ”‚ โ—‹ 601223f5faa8 c 1235 + โ”‚ โ”‚ A c 1236 + โ”‚ โ—‹ 28223a4af36c b 1237 + โ”‚ โ”‚ A a 1238 + โ”‚ โ”‚ A b 1239 + โ—‹ โ”‚ c20a2a7a24ba e 1240 + โ”‚ โ”‚ A e 1241 + โ—‹ โ”‚ a224ba6ebde8 d 1242 + โ”œโ”€โ•ฏ A a 1243 + โ”‚ A d 1244 + โ—‹ 367fe826e43e a (empty) 1245 + โ—† 000000000000 (empty) 1246 + [EOF] 1247 + "); 1248 + let output = work_dir.run_jj(["file", "list", "-r=b"]); 1249 + insta::assert_snapshot!(output, @r" 1250 + a 1251 + b 1252 + [EOF] 1253 + "); 1254 + let output = work_dir.run_jj(["file", "list", "-r=c"]); 1255 + insta::assert_snapshot!(output, @r" 1256 + a 1257 + b 1258 + c 1259 + [EOF] 1260 + "); 1261 + let output = work_dir.run_jj(["file", "list", "-r=d"]); 1262 + insta::assert_snapshot!(output, @r" 1263 + a 1264 + d 1265 + [EOF] 1266 + "); 1267 + 1268 + // --restore-descendants works when squashing from child to parent 1269 + work_dir 1270 + .run_jj(["operation", "restore", &beginning]) 1271 + .success(); 1272 + let output = work_dir.run_jj(["squash", "--from=b", "--into=a", "--restore-descendants"]); 1273 + insta::assert_snapshot!(output, @r" 1274 + ------- stderr ------- 1275 + Rebased 4 descendant commits (while preserving their content) 1276 + Working copy (@) now at: kpqxywon 6ad1c62a f | (no description set) 1277 + Parent commit (@-) : yostqsxw e259f026 e | (no description set) 1278 + Parent commit (@-) : mzvwutvl 36192c59 c | (no description set) 1279 + [EOF] 1280 + "); 1281 + insta::assert_snapshot!(run_log(), @r" 1282 + @ 6ad1c62aec5b f 1283 + โ”œโ”€โ•ฎ A b 1284 + โ”‚ โ”‚ A f 1285 + โ”‚ โ—‹ 36192c59f1e9 c 1286 + โ”‚ โ”‚ A c 1287 + โ—‹ โ”‚ e259f02633ca e 1288 + โ”‚ โ”‚ A e 1289 + โ—‹ โ”‚ 92943f1c8204 d 1290 + โ”œโ”€โ•ฏ D b 1291 + โ”‚ A d 1292 + โ—‹ 59aac8514774 a b 1293 + โ”‚ A a 1294 + โ”‚ A b 1295 + โ—† 000000000000 (empty) 1296 + [EOF] 1297 + "); 1298 + let output = work_dir.run_jj(["file", "list", "-r=b"]); 1299 + insta::assert_snapshot!(output, @r" 1300 + a 1301 + b 1302 + [EOF] 1303 + "); 1304 + let output = work_dir.run_jj(["file", "list", "-r=c"]); 1305 + insta::assert_snapshot!(output, @r" 1306 + a 1307 + b 1308 + c 1309 + [EOF] 1310 + "); 1311 + let output = work_dir.run_jj(["file", "list", "-r=d"]); 1312 + insta::assert_snapshot!(output, @r" 1313 + a 1314 + d 1315 + [EOF] 1316 + "); 1317 + 1318 + // same test, but with --keep-emptied 1319 + work_dir 1320 + .run_jj(["operation", "restore", &beginning]) 1321 + .success(); 1322 + let output = work_dir.run_jj([ 1323 + "squash", 1324 + "--from=b", 1325 + "--into=a", 1326 + "--keep-emptied", 1327 + "--restore-descendants", 1328 + ]); 1329 + insta::assert_snapshot!(output, @r" 1330 + ------- stderr ------- 1331 + Rebased 5 descendant commits (while preserving their content) 1332 + Working copy (@) now at: kpqxywon 6eadede0 f | (no description set) 1333 + Parent commit (@-) : yostqsxw 97233b50 e | (no description set) 1334 + Parent commit (@-) : mzvwutvl 5b2d6858 c | (no description set) 1335 + [EOF] 1336 + "); 1337 + // BUG! b should now be empty! 1338 + insta::assert_snapshot!(run_log(), @r" 1339 + @ 6eadede086b1 f 1340 + โ”œโ”€โ•ฎ A b 1341 + โ”‚ โ”‚ A f 1342 + โ”‚ โ—‹ 5b2d685868b7 c 1343 + โ”‚ โ”‚ A b 1344 + โ”‚ โ”‚ A c 1345 + โ”‚ โ—‹ 904dac9cd09e b 1346 + โ”‚ โ”‚ D b 1347 + โ—‹ โ”‚ 97233b506c11 e 1348 + โ”‚ โ”‚ A e 1349 + โ—‹ โ”‚ 8cbe1a629aed d 1350 + โ”œโ”€โ•ฏ D b 1351 + โ”‚ A d 1352 + โ—‹ c1fbbbe74a28 a 1353 + โ”‚ A a 1354 + โ”‚ A b 1355 + โ—† 000000000000 (empty) 1356 + [EOF] 1357 + "); 1358 + let output = work_dir.run_jj(["file", "list", "-r=b"]); 1359 + insta::assert_snapshot!(output, @r" 1360 + a 1361 + [EOF] 1362 + "); 1363 + let output = work_dir.run_jj(["file", "list", "-r=c"]); 1364 + insta::assert_snapshot!(output, @r" 1365 + a 1366 + b 1367 + c 1368 + [EOF] 1369 + "); 1370 + let output = work_dir.run_jj(["file", "list", "-r=d"]); 1371 + insta::assert_snapshot!(output, @r" 1372 + a 1373 + d 1374 + [EOF] 1375 + "); 1376 + 1377 + // ========== Part 3 ========= 1378 + // Reminder of the setup 1379 + test_env.advance_test_rng_seed_to_multiple_of(200_000); 1380 + work_dir 1381 + .run_jj(["operation", "restore", &beginning]) 1382 + .success(); 1383 + insta::assert_snapshot!(run_log(), @r" 1384 + @ 42acd0537c88 f 1385 + โ”œโ”€โ•ฎ A f 1386 + โ”‚ โ—‹ 4fb9706b0f47 c 1387 + โ”‚ โ”‚ A c 1388 + โ”‚ โ—‹ b1e1eea2f666 b 1389 + โ”‚ โ”‚ A b 1390 + โ—‹ โ”‚ b4e3197108ba e 1391 + โ”‚ โ”‚ A e 1392 + โ—‹ โ”‚ d707102f499f d 1393 + โ”œโ”€โ•ฏ A d 1394 + โ—‹ 7468364c89fc a 1395 + โ”‚ A a 1396 + โ—† 000000000000 (empty) 1397 + [EOF] 1398 + "); 1399 + let output = work_dir.run_jj(["file", "list", "-r=d"]); 1400 + insta::assert_snapshot!(output, @r" 1401 + a 1402 + d 1403 + [EOF] 1404 + "); 1405 + let output = work_dir.run_jj(["file", "list", "-r=f"]); 1406 + insta::assert_snapshot!(output, @r" 1407 + a 1408 + b 1409 + c 1410 + d 1411 + e 1412 + f 1413 + [EOF] 1414 + "); 1415 + 1416 + // --restore-descendants works when squashing from grandchild to grandparent 1417 + work_dir 1418 + .run_jj(["operation", "restore", &beginning]) 1419 + .success(); 1420 + let output = work_dir.run_jj(["squash", "--from=e", "--into=a", "--restore-descendants"]); 1421 + insta::assert_snapshot!(output, @r" 1422 + ------- stderr ------- 1423 + Rebased 4 descendant commits (while preserving their content) 1424 + Working copy (@) now at: kpqxywon 6d14c928 f | (no description set) 1425 + Parent commit (@-) : yqosqzyt ab775412 d e | (no description set) 1426 + Parent commit (@-) : mzvwutvl 175aa1f2 c | (no description set) 1427 + [EOF] 1428 + "); 1429 + insta::assert_snapshot!(run_log(), @r" 1430 + @ 6d14c928f32e f 1431 + โ”œโ”€โ•ฎ A e 1432 + โ”‚ โ”‚ A f 1433 + โ”‚ โ—‹ 175aa1f28a05 c 1434 + โ”‚ โ”‚ A c 1435 + โ”‚ โ—‹ d1076aeca3e6 b 1436 + โ”‚ โ”‚ A b 1437 + โ”‚ โ”‚ D e 1438 + โ—‹ โ”‚ ab7754126332 d e 1439 + โ”œโ”€โ•ฏ A d 1440 + โ”‚ D e 1441 + โ—‹ 4644e0c16443 a 1442 + โ”‚ A a 1443 + โ”‚ A e 1444 + โ—† 000000000000 (empty) 1445 + [EOF] 1446 + "); 1447 + let output = work_dir.run_jj(["file", "list", "-r=b"]); 1448 + insta::assert_snapshot!(output, @r" 1449 + a 1450 + b 1451 + [EOF] 1452 + "); 1453 + let output = work_dir.run_jj(["file", "list", "-r=d"]); 1454 + insta::assert_snapshot!(output, @r" 1455 + a 1456 + d 1457 + [EOF] 1458 + "); 1459 + let output = work_dir.run_jj(["file", "list", "-r=f"]); 1460 + insta::assert_snapshot!(output, @r" 1461 + a 1462 + b 1463 + c 1464 + d 1465 + e 1466 + f 1467 + [EOF] 1468 + "); 1469 + 1470 + // --restore-descendants works when squashing from grandparent to grandchild 1471 + work_dir 1472 + .run_jj(["operation", "restore", &beginning]) 1473 + .success(); 1474 + let output = work_dir.run_jj(["squash", "--from=a", "--into=e", "--restore-descendants"]); 1475 + insta::assert_snapshot!(output, @r" 1476 + ------- stderr ------- 1477 + Rebased 1 descendant commits (while preserving their content) 1478 + Working copy (@) now at: kpqxywon 94ad7042 f | (no description set) 1479 + Parent commit (@-) : yostqsxw 582d640e e | (no description set) 1480 + Parent commit (@-) : mzvwutvl 2214436c c | (no description set) 1481 + [EOF] 1482 + "); 1483 + insta::assert_snapshot!(run_log(), @r" 1484 + @ 94ad70428c4a f 1485 + โ”œโ”€โ•ฎ A f 1486 + โ”‚ โ—‹ 2214436c3fa7 c 1487 + โ”‚ โ”‚ A c 1488 + โ”‚ โ—‹ a469c893f362 b 1489 + โ”‚ โ”‚ A a 1490 + โ”‚ โ”‚ A b 1491 + โ—‹ โ”‚ 582d640e331f e 1492 + โ”‚ โ”‚ A e 1493 + โ—‹ โ”‚ 93671eb30330 d 1494 + โ”œโ”€โ•ฏ A a 1495 + โ”‚ A d 1496 + โ—† 000000000000 a (empty) 1497 + [EOF] 1498 + "); 1499 + let output = work_dir.run_jj(["file", "list", "-r=b"]); 1500 + insta::assert_snapshot!(output, @r" 1501 + a 1502 + b 1503 + [EOF] 1504 + "); 1505 + let output = work_dir.run_jj(["file", "list", "-r=d"]); 1506 + insta::assert_snapshot!(output, @r" 1507 + a 1508 + d 1509 + [EOF] 1510 + "); 1511 + let output = work_dir.run_jj(["file", "list", "-r=f"]); 1512 + insta::assert_snapshot!(output, @r" 1513 + a 1514 + b 1515 + c 1516 + d 1517 + e 1518 + f 1519 + [EOF] 1520 + "); 1521 + } 1522 + 748 1523 #[test] 749 1524 fn test_squash_from_multiple() { 750 1525 let test_env = TestEnvironment::default();
+29 -11
lib/src/rewrite.rs
··· 1109 1109 pub abandoned_commits: Vec<Commit>, 1110 1110 } 1111 1111 1112 + #[derive(Clone, Debug)] 1113 + pub struct SquashOptions { 1114 + pub keep_emptied: bool, 1115 + pub restore_descendants: bool, 1116 + } 1117 + 1112 1118 /// Squash `sources` into `destination` and return a [`SquashedCommit`] for the 1113 1119 /// resulting commit. Caller is responsible for setting the description and 1114 1120 /// finishing the commit. ··· 1116 1122 repo: &'repo mut MutableRepo, 1117 1123 sources: &[CommitWithSelection], 1118 1124 destination: &Commit, 1119 - keep_emptied: bool, 1125 + SquashOptions { 1126 + keep_emptied, 1127 + restore_descendants, 1128 + }: SquashOptions, 1120 1129 ) -> BackendResult<Option<SquashedCommit<'repo>>> { 1121 1130 struct SourceCommit<'a> { 1122 1131 commit: &'a CommitWithSelection, ··· 1169 1178 // rewritten sources. Otherwise it will likely already have the content 1170 1179 // changes we're moving, so applying them will have no effect and the 1171 1180 // changes will disappear. 1172 - let options = RebaseOptions::default(); 1173 - repo.rebase_descendants_with_options(&options, |old_commit, rebased_commit| { 1174 - if old_commit.id() != destination.id() { 1175 - return; 1176 - } 1177 - rewritten_destination = match rebased_commit { 1178 - RebasedCommit::Rewritten(commit) => commit, 1179 - RebasedCommit::Abandoned { .. } => panic!("all commits should be kept"), 1180 - }; 1181 - })?; 1181 + if restore_descendants { 1182 + repo.reparent_descendants_with_progress(|old_commit, rebased_commit| { 1183 + if old_commit.id() != destination.id() { 1184 + return; 1185 + } 1186 + rewritten_destination = rebased_commit; 1187 + })?; 1188 + } else { 1189 + let options = RebaseOptions::default(); 1190 + repo.rebase_descendants_with_options(&options, |old_commit, rebased_commit| { 1191 + if old_commit.id() != destination.id() { 1192 + return; 1193 + } 1194 + rewritten_destination = match rebased_commit { 1195 + RebasedCommit::Rewritten(commit) => commit, 1196 + RebasedCommit::Abandoned { .. } => panic!("all commits should be kept"), 1197 + }; 1198 + })?; 1199 + } 1182 1200 } 1183 1201 // Apply the selected changes onto the destination 1184 1202 let mut destination_tree = rewritten_destination.tree()?;

History

1 round 0 comments
sign up or login to add to the discussion
1 commit
expand
cli squash: new --restore-descendants option
no conflicts, ready to merge
expand 0 comments