just playing with tangled

repo: create a reparent_descendants_with_progress helper (no-op) #7

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

This is by analogy with rebase_descendants_with_options, except we don't need the options.

Labels

None yet.

Participants 1
AT URI
at://did:plc:jp6rly3c67o3zlwarw2ttafu/sh.tangled.repo.pull/3lqh3p4clf322
+977 -14
Diff #0
+19 -4
lib/src/repo.rs
··· 1406 1406 /// The content of those descendants will remain untouched. 1407 1407 /// Returns the number of reparented descendants. 1408 1408 pub fn reparent_descendants(&mut self) -> BackendResult<usize> { 1409 - let roots = self.parent_mapping.keys().cloned().collect_vec(); 1410 1409 let mut num_reparented = 0; 1410 + self.reparent_descendants_with_progress(|_, _| { 1411 + num_reparented += 1; 1412 + })?; 1413 + Ok(num_reparented) 1414 + } 1415 + 1416 + /// Reparent descendants, and call the provided function for each moved 1417 + /// commit 1418 + /// 1419 + /// The function takes the old commit and the reparented commit. 1420 + pub fn reparent_descendants_with_progress( 1421 + &mut self, 1422 + mut progress: impl FnMut(Commit, Commit), 1423 + ) -> BackendResult<()> { 1424 + let roots = self.parent_mapping.keys().cloned().collect_vec(); 1411 1425 self.transform_descendants(roots, |rewriter| { 1412 1426 if rewriter.parents_changed() { 1427 + let old_commit = rewriter.old_commit().clone(); 1413 1428 let builder = rewriter.reparent(); 1414 - builder.write()?; 1415 - num_reparented += 1; 1429 + let reparented_commit = builder.write()?; 1430 + progress(old_commit, reparented_commit); 1416 1431 } 1417 1432 Ok(()) 1418 1433 })?; 1419 1434 self.parent_mapping.clear(); 1420 - Ok(num_reparented) 1435 + Ok(()) 1421 1436 } 1422 1437 1423 1438 pub fn set_wc_commit(
+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
··· 746 746 } 747 747 748 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 5 descendant commits (while preserving their content) 1170 + Working copy (@) now at: kpqxywon 27d75f43 f | (no description set) 1171 + Parent commit (@-) : yostqsxw 102e6106 e | (no description set) 1172 + Parent commit (@-) : mzvwutvl 86d2ecde c | (no description set) 1173 + [EOF] 1174 + "); 1175 + insta::assert_snapshot!(run_log(), @r" 1176 + @ 27d75f43e860 f 1177 + ├─╮ A f 1178 + │ ○ 86d2ecdec2d7 c 1179 + │ │ A c 1180 + │ ○ 7c3b32b0545d 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 5 descendant commits (while preserving their content) 1226 + Working copy (@) now at: kpqxywon a6c6eeb5 f | (no description set) 1227 + Parent commit (@-) : yostqsxw c20a2a7a e | (no description set) 1228 + Parent commit (@-) : mzvwutvl 5230f5a0 c | (no description set) 1229 + [EOF] 1230 + "); 1231 + insta::assert_snapshot!(run_log(), @r" 1232 + @ a6c6eeb5767f f 1233 + ├─╮ A f 1234 + │ ○ 5230f5a06e69 c 1235 + │ │ A c 1236 + │ ○ 5d6fef1e0e34 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 5 descendant commits (while preserving their content) 1478 + Working copy (@) now at: kpqxywon e92b3f0f f | (no description set) 1479 + Parent commit (@-) : yostqsxw 78651b37 e | (no description set) 1480 + Parent commit (@-) : mzvwutvl 2214436c c | (no description set) 1481 + [EOF] 1482 + "); 1483 + insta::assert_snapshot!(run_log(), @r" 1484 + @ e92b3f0fb9fe f 1485 + ├─╮ A f 1486 + │ ○ 2214436c3fa7 c 1487 + │ │ A c 1488 + │ ○ a469c893f362 b 1489 + │ │ A a 1490 + │ │ A b 1491 + ○ │ 78651b37e114 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 + 1523 + #[test] 749 1524 fn test_squash_from_multiple() { 750 1525 let test_env = TestEnvironment::default();
+19 -8
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, ··· 1158 1167 1159 1168 1160 1169 1170 + } 1161 1171 1162 - 1163 - 1164 - 1165 - 1166 - 1167 - 1168 - 1172 + let mut rewritten_destination = destination.clone(); 1173 + if !restore_descendants 1174 + && sources.iter().any(|source| { 1175 + repo.index() 1176 + .is_ancestor(source.commit.id(), destination.id()) 1177 + }) 1178 + { 1179 + // If we're moving changes to a descendant, first rebase descendants onto the 1169 1180 // rewritten sources. Otherwise it will likely already have the content 1170 1181 // changes we're moving, so applying them will have no effect and the 1171 1182 // changes will disappear.

History

1 round 0 comments
sign up or login to add to the discussion
3 commits
expand
repo: create a reparent_descendants_with_progress helper (no-op)
cli squash: new --restore-descendants option
no-restore-descendants
no conflicts, ready to merge
expand 0 comments