···67* Templates can now do arithmetic on integers with the `+`, `-`, `*`, `/`, and `%`
68 infix operators.
690000070* Evolution history is now stored in the operation log. `jj evolog` can show
71 associated operations for commits created by new jj.
72···7879* `jj parallelize` can now parallelize groups of changes that _start_ with an
80 immutable change, but do not contain any other immutable changes.
81-82-* `jj` will no longer warn about deprecated paths on macOS if the configured
83- XDG directory is the deprecated one (~/Library/Application Support).
8485### Packaging changes
86···118 `template-aliases.default_commit_description`. Please also consider using
119 [`templates.draft_commit_description`](docs/config.md#default-description),
120 and/or [`templates.commit_trailers`](docs/config.md#commit-trailers).
121-122-* On macOS, config.toml files in `~/Library/Application Support/jj` are
123- deprecated; one should instead use `$XDG_CONFIG_HOME/jj`
124- (defaults to `~/.config/jj`)
125126### New features
127···326327* The 'how to resolve conflicts' hint that is shown when conflicts appear can
328 be hidden by setting `hints.resolving-conflicts = false`.
000329330* `jj op diff` and `jj op log --op-diff` now show changes to which commits
331 correspond to working copies.
···67* Templates can now do arithmetic on integers with the `+`, `-`, `*`, `/`, and `%`
68 infix operators.
6970+* `jj split` assigns the change id and the bookmarks of the source revision
71+ to the revision with the non-selected changes.
72+ You can opt out of this change by setting `split.legacy-bookmark-behavior = true`,
73+ but this will likely be removed in a future release.
74+75* Evolution history is now stored in the operation log. `jj evolog` can show
76 associated operations for commits created by new jj.
77···8384* `jj parallelize` can now parallelize groups of changes that _start_ with an
85 immutable change, but do not contain any other immutable changes.
0008687### Packaging changes
88···120 `template-aliases.default_commit_description`. Please also consider using
121 [`templates.draft_commit_description`](docs/config.md#default-description),
122 and/or [`templates.commit_trailers`](docs/config.md#commit-trailers).
0000123124### New features
125···324325* The 'how to resolve conflicts' hint that is shown when conflicts appear can
326 be hidden by setting `hints.resolving-conflicts = false`.
327+328+* `jj squash` now has a `--restore-descendants` option to preserve the snapshots
329+ of the children of the modified commits.
330331* `jj op diff` and `jj op log --op-diff` now show changes to which commits
332 correspond to working copies.
···61 "log",
62 "--no-graph",
63 "-r=@-",
64+ "-T=commit_id ++ '-'",
65 ])
66 .output()
67 {
68 if output.status.success() {
69+ let mut parent_commits = String::from_utf8(output.stdout).unwrap();
70+ // TODO(ilyagr): The `test_version` integration test shoult be fixed
71+ // to succeed even if there is more than one parent commit.
72+ parent_commits.truncate(parent_commits.trim_end_matches('-').len());
73+ return Some(parent_commits);
74 }
75 }
76
+3-13
cli/src/cli_util.rs
···3858 "Did you update to a commit where the directory doesn't exist?",
3859 )
3860 })?;
3861- let mut config_env = ConfigEnv::from_environment(ui);
3862 let mut last_config_migration_descriptions = Vec::new();
3863 let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
3864 last_config_migration_descriptions =
···3937 ui.reset(&config)?;
39383939 // Print only the last migration messages to omit duplicates.
3940- for (source, desc) in &last_config_migration_descriptions {
3941- let source_str = match source {
3942- ConfigSource::Default => "default-provided",
3943- ConfigSource::EnvBase | ConfigSource::EnvOverrides => "environment-provided",
3944- ConfigSource::User => "user-level",
3945- ConfigSource::Repo => "repo-level",
3946- ConfigSource::CommandArg => "CLI-provided",
3947- };
3948- writeln!(
3949- ui.warning_default(),
3950- "Deprecated {source_str} config: {desc}"
3951- )?;
3952 }
39533954 if args.global_args.repository.is_some() {
···3858 "Did you update to a commit where the directory doesn't exist?",
3859 )
3860 })?;
3861+ let mut config_env = ConfigEnv::from_environment();
3862 let mut last_config_migration_descriptions = Vec::new();
3863 let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
3864 last_config_migration_descriptions =
···3937 ui.reset(&config)?;
39383939 // Print only the last migration messages to omit duplicates.
3940+ for desc in &last_config_migration_descriptions {
3941+ writeln!(ui.warning_default(), "Deprecated config: {desc}")?;
00000000003942 }
39433944 if args.global_args.repository.is_some() {
+6-1
cli/src/commands/commit.rs
···12// See the License for the specific language governing permissions and
13// limitations under the License.
14015use clap_complete::ArgValueCompleter;
16use indoc::writedoc;
17use jj_lib::backend::Signature;
···37 #[arg(short, long)]
38 interactive: bool,
39 /// Specify diff editor to be used (implies --interactive)
40- #[arg(long, value_name = "NAME")]
000041 tool: Option<String>,
42 /// The change description to use (don't open editor)
43 #[arg(long = "message", short, value_name = "MESSAGE")]
···12// See the License for the specific language governing permissions and
13// limitations under the License.
1415+use clap_complete::ArgValueCandidates;
16use clap_complete::ArgValueCompleter;
17use indoc::writedoc;
18use jj_lib::backend::Signature;
···38 #[arg(short, long)]
39 interactive: bool,
40 /// Specify diff editor to be used (implies --interactive)
41+ #[arg(
42+ long,
43+ value_name = "NAME",
44+ add = ArgValueCandidates::new(complete::diff_editors),
45+ )]
46 tool: Option<String>,
47 /// The change description to use (don't open editor)
48 #[arg(long = "message", short, value_name = "MESSAGE")]
+6-1
cli/src/commands/diffedit.rs
···1415use std::io::Write as _;
16017use clap_complete::ArgValueCompleter;
18use itertools::Itertools as _;
19use jj_lib::matchers::EverythingMatcher;
···77 )]
78 to: Option<RevisionArg>,
79 /// Specify diff editor to be used
80- #[arg(long, value_name = "NAME")]
000081 tool: Option<String>,
82 /// Preserve the content (not the diff) when rebasing descendants
83 ///
···1415use std::io::Write as _;
1617+use clap_complete::ArgValueCandidates;
18use clap_complete::ArgValueCompleter;
19use itertools::Itertools as _;
20use jj_lib::matchers::EverythingMatcher;
···78 )]
79 to: Option<RevisionArg>,
80 /// Specify diff editor to be used
81+ #[arg(
82+ long,
83+ value_name = "NAME",
84+ add = ArgValueCandidates::new(complete::diff_editors),
85+ )]
86 tool: Option<String>,
87 /// Preserve the content (not the diff) when rebasing descendants
88 ///
+60-9
cli/src/commands/evolog.rs
···19use clap_complete::ArgValueCompleter;
20use itertools::Itertools as _;
21use jj_lib::commit::Commit;
022use jj_lib::evolution::walk_predecessors;
23use jj_lib::graph::reverse_graph;
24use jj_lib::graph::GraphEdge;
25use jj_lib::matchers::EverythingMatcher;
0026use tracing::instrument;
2728use super::log::get_node_template;
···32use crate::cli_util::RevisionArg;
33use crate::command_error::CommandError;
34use crate::complete;
035use crate::diff_util::DiffFormatArgs;
36use crate::graphlog::get_graphlog;
37use crate::graphlog::GraphStyle;
···85 /// contaminated by unrelated changes.
86 #[arg(long, short = 'p')]
87 patch: bool,
00000000000000000000088 #[command(flatten)]
89 diff_format: DiffFormatArgs,
90}
···99100 let start_commit = workspace_command.resolve_single_rev(ui, &args.revision)?;
101102- let diff_renderer = workspace_command.diff_renderer_for_log(&args.diff_format, args.patch)?;
0103 let graph_style = GraphStyle::from_settings(workspace_command.settings())?;
104 let with_content_format = LogContentFormat::new(ui, workspace_command.settings())?;
105···173 if let Some(renderer) = &diff_renderer {
174 let predecessors: Vec<_> = entry.predecessors().try_collect()?;
175 let mut formatter = ui.new_formatter(&mut buffer);
176- renderer.show_inter_diff(
177- ui,
178- formatter.as_mut(),
179- &predecessors,
180- &entry.commit,
181- &EverythingMatcher,
182- within_graph.width(),
183- )?;
000000000000000000000000184 }
185 let node_symbol = format_template(ui, &Some(entry.commit.clone()), &node_template);
186 graph.add_node(
···212 if let Some(renderer) = &diff_renderer {
213 let predecessors: Vec<_> = entry.predecessors().try_collect()?;
214 let width = ui.term_width();
0215 renderer.show_inter_diff(
216 ui,
217 formatter,
···189 branches: &[StringPattern],
190 remotes: &[&RemoteName],
191) -> Result<(), CommandError> {
0192 for branch in branches {
193 let matches = remotes.iter().any(|&remote| {
194 let remote = StringPattern::exact(remote);
···205 .is_some()
206 });
207 if !matches {
208- writeln!(
209- ui.warning_default(),
210- "No branch matching `{branch}` found on any specified/configured remote",
211- )?;
212 }
00000000213 }
214215 Ok(())
···189 branches: &[StringPattern],
190 remotes: &[&RemoteName],
191) -> Result<(), CommandError> {
192+ let mut missing_branches = vec![];
193 for branch in branches {
194 let matches = remotes.iter().any(|&remote| {
195 let remote = StringPattern::exact(remote);
···206 .is_some()
207 });
208 if !matches {
209+ missing_branches.push(branch);
000210 }
211+ }
212+213+ if !missing_branches.is_empty() {
214+ writeln!(
215+ ui.warning_default(),
216+ "No branch matching {} found on any specified/configured remote",
217+ missing_branches.iter().map(|b| format!("`{b}`")).join(", ")
218+ )?;
219 }
220221 Ok(())
+7-1
cli/src/commands/resolve.rs
···12// See the License for the specific language governing permissions and
13// limitations under the License.
14015use clap_complete::ArgValueCompleter;
16use itertools::Itertools as _;
17use jj_lib::object_id::ObjectId as _;
···61 ///
62 /// The built-in merge tools `:ours` and `:theirs` can be used to choose
63 /// side #1 and side #2 of the conflict respectively.
64- #[arg(long, conflicts_with = "list", value_name = "NAME")]
0000065 tool: Option<String>,
66 /// Only resolve conflicts in these paths. You can use the `--list` argument
67 /// to find paths to use here.
···12// See the License for the specific language governing permissions and
13// limitations under the License.
1415+use clap_complete::ArgValueCandidates;
16use clap_complete::ArgValueCompleter;
17use itertools::Itertools as _;
18use jj_lib::object_id::ObjectId as _;
···62 ///
63 /// The built-in merge tools `:ours` and `:theirs` can be used to choose
64 /// side #1 and side #2 of the conflict respectively.
65+ #[arg(
66+ long,
67+ conflicts_with = "list",
68+ value_name = "NAME",
69+ add = ArgValueCandidates::new(complete::merge_tools),
70+ )]
71 tool: Option<String>,
72 /// Only resolve conflicts in these paths. You can use the `--list` argument
73 /// to find paths to use here.
+6-1
cli/src/commands/restore.rs
···1415use std::io::Write as _;
16017use clap_complete::ArgValueCompleter;
18use indoc::formatdoc;
19use itertools::Itertools as _;
···96 #[arg(long, short)]
97 interactive: bool,
98 /// Specify diff editor to be used (implies --interactive)
99- #[arg(long, value_name = "NAME")]
0000100 tool: Option<String>,
101 /// Preserve the content (not the diff) when rebasing descendants
102 #[arg(long)]
···1415use std::io::Write as _;
1617+use clap_complete::ArgValueCandidates;
18use clap_complete::ArgValueCompleter;
19use indoc::formatdoc;
20use itertools::Itertools as _;
···97 #[arg(long, short)]
98 interactive: bool,
99 /// Specify diff editor to be used (implies --interactive)
100+ #[arg(
101+ long,
102+ value_name = "NAME",
103+ add = ArgValueCandidates::new(complete::diff_editors),
104+ )]
105 tool: Option<String>,
106 /// Preserve the content (not the diff) when rebasing descendants
107 #[arg(long)]
+15-11
cli/src/commands/split.rs
···14use std::collections::HashMap;
15use std::io::Write as _;
16017use clap_complete::ArgValueCompleter;
18use jj_lib::backend::CommitId;
19use jj_lib::commit::Commit;
···49///
50/// Starts a [diff editor] on the changes in the revision. Edit the right side
51/// of the diff until it has the content you want in the new revision. Once
52-/// you close the editor, your edited content will replace the previous
53-/// revision. The remaining changes will be put in a new revision on top.
054///
55/// [diff editor]:
56/// https://jj-vcs.github.io/jj/latest/config/#editing-diffs
···70 #[arg(long, short)]
71 interactive: bool,
72 /// Specify diff editor to be used (implies --interactive)
73- #[arg(long, value_name = "NAME")]
000074 tool: Option<String>,
75 /// The revision to split
76 #[arg(
···219 // Prompt the user to select the changes they want for the first commit.
220 let target = select_diff(ui, &tx, &target_commit, &matcher, &diff_selector)?;
221000222 // Create the first commit, which includes the changes selected by the user.
223 let first_commit = {
224 let mut commit_builder = tx.repo_mut().rewrite_commit(&target.commit).detach();
225 commit_builder.set_tree_id(target.selected_tree.id());
226- if use_move_flags {
227 commit_builder
228 // Generate a new change id so that the commit being split doesn't
229 // become divergent.
···270 commit_builder
271 .set_parents(parents)
272 .set_tree_id(new_tree.id());
273- if !use_move_flags {
274 commit_builder
275 // Generate a new change id so that the commit being split doesn't
276 // become divergent.
···407 tx.repo_mut()
408 .transform_descendants(vec![target.commit.id().clone()], |mut rewriter| {
409 num_rebased += 1;
410- if parallel && legacy_bookmark_behavior {
411- // The old_parent is the second commit due to the rewrite above.
412 rewriter
413 .replace_parent(second_commit.id(), [first_commit.id(), second_commit.id()]);
414- } else if parallel {
415- rewriter.replace_parent(first_commit.id(), [first_commit.id(), second_commit.id()]);
416- } else {
417- rewriter.replace_parent(first_commit.id(), [second_commit.id()]);
418 }
419 rewriter.rebase()?.write()?;
420 Ok(())
···14use std::collections::HashMap;
15use std::io::Write as _;
1617+use clap_complete::ArgValueCandidates;
18use clap_complete::ArgValueCompleter;
19use jj_lib::backend::CommitId;
20use jj_lib::commit::Commit;
···50///
51/// Starts a [diff editor] on the changes in the revision. Edit the right side
52/// of the diff until it has the content you want in the new revision. Once
53+/// you close the editor, your edited content will be put in a new revision
54+/// before the original revision, while the remaining changes will replace the
55+/// original revision.
56///
57/// [diff editor]:
58/// https://jj-vcs.github.io/jj/latest/config/#editing-diffs
···72 #[arg(long, short)]
73 interactive: bool,
74 /// Specify diff editor to be used (implies --interactive)
75+ #[arg(
76+ long,
77+ value_name = "NAME",
78+ add = ArgValueCandidates::new(complete::diff_editors),
79+ )]
80 tool: Option<String>,
81 /// The revision to split
82 #[arg(
···225 // Prompt the user to select the changes they want for the first commit.
226 let target = select_diff(ui, &tx, &target_commit, &matcher, &diff_selector)?;
227228+ let legacy_bookmark_behavior =
229+ !use_move_flags && tx.settings().get_bool("split.legacy-bookmark-behavior")?;
230+231 // Create the first commit, which includes the changes selected by the user.
232 let first_commit = {
233 let mut commit_builder = tx.repo_mut().rewrite_commit(&target.commit).detach();
234 commit_builder.set_tree_id(target.selected_tree.id());
235+ if !legacy_bookmark_behavior {
236 commit_builder
237 // Generate a new change id so that the commit being split doesn't
238 // become divergent.
···279 commit_builder
280 .set_parents(parents)
281 .set_tree_id(new_tree.id());
282+ if legacy_bookmark_behavior {
283 commit_builder
284 // Generate a new change id so that the commit being split doesn't
285 // become divergent.
···416 tx.repo_mut()
417 .transform_descendants(vec![target.commit.id().clone()], |mut rewriter| {
418 num_rebased += 1;
419+ if parallel {
0420 rewriter
421 .replace_parent(second_commit.id(), [first_commit.id(), second_commit.id()]);
0000422 }
423 rewriter.rebase()?.write()?;
424 Ok(())
+162-3
cli/src/commands/squash.rs
···12// See the License for the specific language governing permissions and
13// limitations under the License.
14015use clap_complete::ArgValueCompleter;
16use indoc::formatdoc;
17use itertools::Itertools as _;
···22use jj_lib::repo::Repo as _;
23use jj_lib::rewrite;
24use jj_lib::rewrite::CommitWithSelection;
025use tracing::instrument;
2627use crate::cli_util::CommandHelper;
···99 #[arg(long, short)]
100 interactive: bool,
101 /// Specify diff editor to be used (implies --interactive)
102- #[arg(long, value_name = "NAME")]
0000103 tool: Option<String>,
104 /// Move only changes to these paths (instead of all paths)
105 #[arg(
···112 /// The source revision will not be abandoned
113 #[arg(long, short)]
114 keep_emptied: bool,
000000000000000000115}
116000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000117#[instrument(skip_all)]
118pub(crate) fn cmd_squash(
119 ui: &mut Ui,
···166 .check_rewritable(sources.iter().chain(std::iter::once(&destination)).ids())?;
167168 let mut tx = workspace_command.start_transaction();
169- let tx_description = format!("squash commits into {}", destination.id().hex());
00000000170 let source_commits = select_diff(&tx, &sources, &destination, &matcher, &diff_selector)?;
171 if let Some(squashed) = rewrite::squash_commits(
172 tx.repo_mut(),
173 &source_commits,
174 &destination,
175- args.keep_emptied,
00000176 )? {
177 let mut commit_builder = squashed.commit_builder.detach();
178 let new_description = match description {
···220 };
221 commit_builder.set_description(new_description);
222 commit_builder.write(tx.repo_mut())?;
000000000000223 } else {
224 if diff_selector.is_interactive() {
225 return Err(user_error("No changes selected"));
···241 }
242 }
243 }
00244 tx.finish(ui, tx_description)?;
245 Ok(())
246}
···12// See the License for the specific language governing permissions and
13// limitations under the License.
1415+use clap_complete::ArgValueCandidates;
16use clap_complete::ArgValueCompleter;
17use indoc::formatdoc;
18use itertools::Itertools as _;
···23use jj_lib::repo::Repo as _;
24use jj_lib::rewrite;
25use jj_lib::rewrite::CommitWithSelection;
26+use jj_lib::rewrite::SquashOptions;
27use tracing::instrument;
2829use crate::cli_util::CommandHelper;
···101 #[arg(long, short)]
102 interactive: bool,
103 /// Specify diff editor to be used (implies --interactive)
104+ #[arg(
105+ long,
106+ value_name = "NAME",
107+ add = ArgValueCandidates::new(complete::diff_editors),
108+ )]
109 tool: Option<String>,
110 /// Move only changes to these paths (instead of all paths)
111 #[arg(
···118 /// The source revision will not be abandoned
119 #[arg(long, short)]
120 keep_emptied: bool,
121+ /// Preserve the content (not the diff) when rebasing descendants of the
122+ /// source and target commits
123+ ///
124+ /// Only the snapshots of the `--from` and the `--into` commits will be
125+ /// modified.
126+ ///
127+ /// If you'd like to preserve the content of *only* the target's descendants
128+ /// (or *only* the source's), consider using `jj rebase -r` or `jj
129+ /// duplicate` before squashing.
130+ //
131+ // See "NOTE: Not implementing `--restore-{target,source}-descendants`" in
132+ // squash.rs.
133+ //
134+ // TODO: Once it's implemented, we should recommend `jj rebase -r
135+ // --restore-descendants` instead of `jj duplicate`, since you actually
136+ // would need to `squash` twice with `duplicate`.
137+ #[arg(long)]
138+ restore_descendants: bool,
139}
140141+// NOTE: Not implementing `--restore-{target,source}-descendants`
142+// --------------------------------------------------------------
143+//
144+// We have `jj squash --restore-descendants --from X --into Y` preserve the
145+// snapshots of both the descendants of `X` and those of the descendants of `Y`.
146+// This behavior makes it simple to understand; it does the same thing to the
147+// child of any commit `jj squash` rewrites. As @yuja pointed out it could even
148+// be a global flag that would apply to any command that rewrites commits.
149+//
150+// In this note, we explain why we choose not to have a flag for `jj squash`
151+// that preserves *only* the descendants of the source (call it
152+// `--restore-source-descendants`) or a similar `--restore-target-descendants`
153+// flag, even though they might seem easy to implement at a glance.
154+//
155+// (The same argument applies to `jj rebase --restore-???-descendants`.)
156+//
157+// Firstly, such extra flags seem to only be useful in rare cases. If needed,
158+// they can be simulated. Instead of `squash --restore-target-descendants`, you
159+// could do `jj rebase -r X -d all:X-; jj squash --restore-descendants --from X
160+// --into Y`. Instead of `squash --restore-source-descendants`, you could do `jj
161+// duplicate -r X; jj squash --restore-descendants --from copy_of_X --into Y; jj
162+// abandon --restore-descendants X`. (TODO: When `jj rebase -r
163+// --restore-descendants` is implemented, this will become 2 commands instead of
164+// 3).
165+//
166+// Secondly, the behavior of these flags would get confusing in corner cases,
167+// when the target is an ancestor or descendant of the source, or for ancestors
168+// of merge commits. For example, consider this commit graph with merge commit
169+// `Z` where `A` is *not* empty (thanks to @lilyball for suggesting the merge
170+// commit example):
171+//
172+// ```
173+// A -> X -
174+// \ (Example I)
175+// B -> Y --->Z
176+// ```
177+//
178+// The behavior of `jj squash --from A --into B --restore-descendants` is easy
179+// to understand: the snapshots of `X` and `Y` remain the same, and all of their
180+// descendants also remain the same by normal rebasing rules.
181+//
182+// If we allowed `jj squash --from A --into B --restore-target-descendants`,
183+// what should it mean? It seems clear that `X`'s snapshot should remain the
184+// same, and `X`'s will change. However, should `Z`'s snapshot change? If we
185+// follow the logic that Z had one of its parents change and the other stay the
186+// same, it seems that yes, it should. This is also what the equivalence with
187+// `jj rebase -r A -d A-; jj squash --from A --into B --restore-descendants`
188+// would imply.
189+//
190+// (A contrarian mind could argue that `Z`'s snapshot should be preserved since
191+// `Z` is a descendant of the target `B`. We'll put this thought aside for a
192+// moment and keep going, to see how things get even more confusing.)
193+//
194+// Now, let's pretend we squashed `X` and `Y` into `Z` and ask the same
195+// question. Our graph is now:
196+//
197+// ```
198+// A -
199+// \ (Example II)
200+// B --->Z
201+// ```
202+//
203+// By the logic above, the snapshot of `Z` will again change after `jj squash
204+// --from A --into B --restore-target-descendants`. This is unsatisfying and
205+// would probably be unexpected, since `Z` is a direct child of the target
206+// commit `B`, so the user might expect its snapshot to be preserved.
207+//
208+// Now, there are a few options:
209+//
210+// 1. Allow the confusing but seemingly correct definition of
211+// `--restore-target-descendants` as above.
212+// 2. Allow `--restore-target-descendants`, but forbid it in some set of
213+// situations we deem too confusing.
214+// 3. Have the effect of `jj squash --from A --into B
215+// --restore-target-descendants` on `Z`'s snapshot differ between Example I
216+// and Example II. In other words, the behavior will depend on whether there
217+// are commits (even if they are empty commits!) between `A` and `Z`, or
218+// between `B` and `Z`.
219+// 4. Declare that in both Example I and Example II above, the snapshot of `Z`
220+// should be preserved.
221+//
222+// The first problem with this (and with option 3 above) would be that
223+// `--restore-target-descendants` would now be equivalent to a rebase
224+// followed by `squash --restore-descendants` *almost* always, but would
225+// differ in corner cases.
226+//
227+// Perhaps more importantly, this would break the important property of `jj
228+// squash --restore-target-descendants` that its difference from the
229+// behavior of normal `jj squash` is local; affects only the direct children
230+// of the modified commits. All others can normally be rebased by normal
231+// `jj` rules.
232+//
233+// If `jj squash --restore-target-descendants` preserved the snapshot of `Z`
234+// even if there are 100 commit between it and `A`, this would change its
235+// diff relative to its parents, possibly without any awareness from the
236+// user that this happened or that `Z` even existed.
237+// 5. Do not provide `--restore-target-descendants` ourselves, and recommend
238+// that the user manually does `jj rebase -r X -d all:X-; jj squash
239+// --restore-descendants --from X --into Y` if they really need it.
240+//
241+// The last option seems easiest. It also has the advantage of requiring fewer
242+// tests and being the simplest to maintain.
243+//
244+// Aside: the merge example is probably the easiest to understand and the most
245+// problematic, but for `X -> A -> B -> C -> D`, both `jj squash --from C
246+// --into A --restore-target-descendants` and `jj squash --from A --into C
247+// --restore-source-descendants` have similar problems.
248+249#[instrument(skip_all)]
250pub(crate) fn cmd_squash(
251 ui: &mut Ui,
···298 .check_rewritable(sources.iter().chain(std::iter::once(&destination)).ids())?;
299300 let mut tx = workspace_command.start_transaction();
301+ let tx_description = format!(
302+ "squash commits into {}{}",
303+ destination.id().hex(),
304+ if args.restore_descendants {
305+ " while preserving descendant contents"
306+ } else {
307+ ""
308+ }
309+ );
310 let source_commits = select_diff(&tx, &sources, &destination, &matcher, &diff_selector)?;
311 if let Some(squashed) = rewrite::squash_commits(
312 tx.repo_mut(),
313 &source_commits,
314 &destination,
315+ SquashOptions {
316+ keep_emptied: args.keep_emptied,
317+ // See "NOTE: Not implementing `--restore-{target,source}-descendants`" in
318+ // squash.rs.
319+ restore_descendants: args.restore_descendants,
320+ },
321 )? {
322 let mut commit_builder = squashed.commit_builder.detach();
323 let new_description = match description {
···365 };
366 commit_builder.set_description(new_description);
367 commit_builder.write(tx.repo_mut())?;
368+369+ if args.restore_descendants {
370+ // If !args.restore_descendants, the corresponding steps are done inside
371+ // tx.finish()
372+ let num_reparented = tx.repo_mut().reparent_descendants()?;
373+ if let Some(mut formatter) = ui.status_formatter() {
374+ writeln!(
375+ formatter,
376+ "Rebased {num_reparented} descendant commits (while preserving their content)",
377+ )?;
378+ }
379+ }
380 } else {
381 if diff_selector.is_interactive() {
382 return Err(user_error("No changes selected"));
···398 }
399 }
400 }
401+ // TODO: Show the "Rebase NNN descendant commits message", add " (while
402+ // preserving their content)" in the --restore-descendants mode
403 tx.finish(ui, tx_description)?;
404 Ok(())
405}
+49-1
cli/src/complete.rs
···34use crate::config::ConfigArgKind;
35use crate::config::ConfigEnv;
36use crate::config::CONFIG_SCHEMA;
0037use crate::revset_util::load_revset_aliases;
38use crate::ui::Ui;
39···474 .collect())
475 })
476}
0000000000000000000000000000000000000000000000477478fn config_keys_rec(
479 prefix: ConfigNamePathBuf,
···895 .and_then(dunce::canonicalize)
896 .map_err(user_error)?;
897 // No config migration for completion. Simply ignore deprecated variables.
898- let mut config_env = ConfigEnv::from_environment(&ui);
899 let maybe_cwd_workspace_loader = DefaultWorkspaceLoaderFactory.create(find_workspace_dir(&cwd));
900 let _ = config_env.reload_user_config(&mut raw_config);
901 if let Ok(loader) = &maybe_cwd_workspace_loader {
···34use crate::config::ConfigArgKind;
35use crate::config::ConfigEnv;
36use crate::config::CONFIG_SCHEMA;
37+use crate::merge_tools::configured_merge_tools;
38+use crate::merge_tools::MergeEditor;
39use crate::revset_util::load_revset_aliases;
40use crate::ui::Ui;
41···476 .collect())
477 })
478}
479+pub fn merge_tools() -> Vec<CompletionCandidate> {
480+ with_jj(|_, settings| {
481+ Ok([":builtin", ":ours", ":theirs"]
482+ .into_iter()
483+ .chain(
484+ configured_merge_tools(settings)
485+ .filter(|name| MergeEditor::dummy_with_name(name, settings).is_ok()),
486+ )
487+ .map(CompletionCandidate::new)
488+ .collect())
489+ })
490+}
491+492+/// Approximate list of known diff editors
493+///
494+/// Diff tools can be used without configuration. Some merge tools that are
495+/// configured for 3-way merging may not work for diffing/diff editing, and we
496+/// can't tell which these are. So, this not reliable, but probably good enough
497+/// for command-line completion.
498+pub fn diff_editors() -> Vec<CompletionCandidate> {
499+ let builtin_format_kinds: Vec<String> = crate::diff_util::BuiltinFormatKind::ALL_VARIANTS
500+ .iter()
501+ .map(|kind| format!(":{}", kind.to_arg_name()))
502+ .collect();
503+ with_jj(|_, settings| {
504+ Ok(std::iter::once(":builtin")
505+ .chain(builtin_format_kinds.iter().map(|s| s.as_str()))
506+ .chain(configured_merge_tools(settings))
507+ .map(CompletionCandidate::new)
508+ .collect())
509+ })
510+}
511+512+/// Approximate list of known diff tools
513+///
514+/// Diff tools can be used without configuration. Some merge tools that are
515+/// configured for 3-way merging may not work for diffing/diff editing, and we
516+/// can't tell which these are. So, this not reliable, but probably good enough
517+/// for command-line completion.
518+pub fn diff_tools() -> Vec<CompletionCandidate> {
519+ with_jj(|_, settings| {
520+ Ok(configured_merge_tools(settings)
521+ .map(CompletionCandidate::new)
522+ .collect())
523+ })
524+}
525526fn config_keys_rec(
527 prefix: ConfigNamePathBuf,
···943 .and_then(dunce::canonicalize)
944 .map_err(user_error)?;
945 // No config migration for completion. Simply ignore deprecated variables.
946+ let mut config_env = ConfigEnv::from_environment();
947 let maybe_cwd_workspace_loader = DefaultWorkspaceLoaderFactory.create(find_workspace_dir(&cwd));
948 let _ = config_env.reload_user_config(&mut raw_config);
949 if let Ok(loader) = &maybe_cwd_workspace_loader {
+1-1
cli/src/config/misc.toml
···54# The behavior when this flag is set to false is experimental and may be changed
55# in the future.
56[split]
57-legacy-bookmark-behavior = true
···54# The behavior when this flag is set to false is experimental and may be changed
55# in the future.
56[split]
57+legacy-bookmark-behavior = false
+5-34
cli/src/config.rs
···43use crate::command_error::config_error_with_message;
44use crate::command_error::CommandError;
45use crate::text_util;
46-use crate::ui::Ui;
4748// TODO(#879): Consider generating entire schema dynamically vs. static file.
49pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json");
···193 fn as_path(&self) -> &Path {
194 &self.path
195 }
0196 fn exists(&self) -> bool {
197 match self.state {
198 ConfigPathState::Exists => true,
···218#[derive(Clone, Default, Debug)]
219struct UnresolvedConfigEnv {
220 config_dir: Option<PathBuf>,
221- // TODO: remove after jj 0.35
222 macos_legacy_config_dir: Option<PathBuf>,
223 home_dir: Option<PathBuf>,
224 jj_config: Option<String>,
225}
226227impl UnresolvedConfigEnv {
228- fn resolve(self, ui: &Ui) -> Vec<ConfigPath> {
229 if let Some(paths) = self.jj_config {
230 return split_paths(&paths)
231 .filter(|path| !path.as_os_str().is_empty())
···284285 if let Some(path) = legacy_platform_config_path {
286 if path.exists() {
287- Self::warn_for_deprecated_path(
288- ui,
289- path.as_path(),
290- "~/Library/Application Support/jj",
291- "~/.config/jj",
292- );
293 paths.push(path);
294 }
295 }
296 if let Some(path) = legacy_platform_config_dir {
297 if path.exists() {
298- Self::warn_for_deprecated_path(
299- ui,
300- path.as_path(),
301- "~/Library/Application Support/jj",
302- "~/.config/jj",
303- );
304 paths.push(path);
305 }
306 }
307308 paths
309 }
310-311- fn warn_for_deprecated_path(ui: &Ui, path: &Path, old: &str, new: &str) {
312- let _ = indoc::writedoc!(
313- ui.warning_default(),
314- r"
315- Deprecated configuration file `{}`.
316- Configuration files in `{old}` are deprecated, and support will be removed in a future release.
317- Instead, move your configuration files to `{new}`.
318- ",
319- path.display(),
320- );
321- }
322}
323324#[derive(Clone, Debug)]
···332333impl ConfigEnv {
334 /// Initializes configuration loader based on environment variables.
335- pub fn from_environment(ui: &Ui) -> Self {
336 let config_dir = etcetera::choose_base_strategy()
337 .ok()
338 .map(|s| s.config_dir());
···347 // Library/Preferences is supposed to be exclusively plists
348 s.data_dir()
349 })
350- .filter(|data_dir| {
351- // User might've purposefully set their config dir to the deprecated one
352- Some(data_dir) != config_dir.as_ref()
353- })
354 } else {
355 None
356 };
···370 ConfigEnv {
371 home_dir,
372 repo_path: None,
373- user_config_paths: env.resolve(ui),
374 repo_config_path: None,
375 command: None,
376 }
···1757 ConfigEnv {
1758 home_dir,
1759 repo_path: None,
1760- user_config_paths: env.resolve(&Ui::null()),
1761 repo_config_path: None,
1762 command: None,
1763 }
···2324use bstr::BStr;
25use bstr::BString;
026use futures::executor::block_on_stream;
27use futures::stream::BoxStream;
28use futures::StreamExt as _;
···124 ///
125 /// A builtin format can also be specified as `:<name>`. For example,
126 /// `--tool=:git` is equivalent to `--git`.
127- #[arg(long)]
000128 pub tool: Option<String>,
129 /// Number of lines of context to show
130 #[arg(long)]
···152}
153154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
155-enum BuiltinFormatKind {
156 Summary,
157 Stat,
158 Types,
···162}
163164impl BuiltinFormatKind {
000000000000165 fn from_name(name: &str) -> Result<Self, String> {
166 match name {
167 "summary" => Ok(Self::Summary),
···205 }
206 }
207208- fn to_arg_name(self) -> &'static str {
209 match self {
210 Self::Summary => "summary",
211 Self::Stat => "stat",
···2324use bstr::BStr;
25use bstr::BString;
26+use clap_complete::ArgValueCandidates;
27use futures::executor::block_on_stream;
28use futures::stream::BoxStream;
29use futures::StreamExt as _;
···125 ///
126 /// A builtin format can also be specified as `:<name>`. For example,
127 /// `--tool=:git` is equivalent to `--git`.
128+ #[arg(
129+ long,
130+ add = ArgValueCandidates::new(crate::complete::diff_tools),
131+ )]
132 pub tool: Option<String>,
133 /// Number of lines of context to show
134 #[arg(long)]
···156}
157158#[derive(Clone, Copy, Debug, Eq, PartialEq)]
159+pub enum BuiltinFormatKind {
160 Summary,
161 Stat,
162 Types,
···166}
167168impl BuiltinFormatKind {
169+ // Alternatively, we could use or vendor one of the crates `strum`,
170+ // `enum-iterator`, or `variant_count` (for a check that the length of the array
171+ // is correct). The latter is very simple and is also a nightly feature.
172+ pub const ALL_VARIANTS: &[BuiltinFormatKind] = &[
173+ Self::Summary,
174+ Self::Stat,
175+ Self::Types,
176+ Self::NameOnly,
177+ Self::Git,
178+ Self::ColorWords,
179+ ];
180+181 fn from_name(name: &str) -> Result<Self, String> {
182 match name {
183 "summary" => Ok(Self::Summary),
···221 }
222 }
223224+ pub fn to_arg_name(self) -> &'static str {
225 match self {
226 Self::Summary => "summary",
227 Self::Stat => "stat",
+22-6
cli/src/merge_tools/mod.rs
···207 }
208}
20900000210/// Loads external diff/merge tool options from `[merge-tools.<name>]`.
211pub fn get_external_tool_config(
212 settings: &UserSettings,
···378 let tool = MergeTool::get_tool_config(settings, name)?
379 .unwrap_or_else(|| MergeTool::external(ExternalMergeTool::with_program(name)));
380 Self::new_inner(name, tool, path_converter, conflict_marker_style)
0000000000000000381 }
382383 /// Loads the default 3-way merge editor from the settings.
···765 let get = |name, config_text| {
766 let config = config_from_string(config_text);
767 let settings = UserSettings::from_config(config).unwrap();
768- let path_converter = RepoPathUiConverter::Fs {
769- cwd: "".into(),
770- base: "".into(),
771- };
772- MergeEditor::with_name(name, &settings, path_converter, ConflictMarkerStyle::Diff)
773- .map(|editor| editor.tool)
774 };
775776 insta::assert_debug_snapshot!(get(":builtin", "").unwrap(), @"Builtin");
···906* `-p`, `--patch` โ Show patch compared to the previous version of this change
907908 If the previous version has different parents, it will be temporarily rebased to the parents of the new version, so the diff is not contaminated by unrelated changes.
000000000909* `-s`, `--summary` โ For each path, show only whether it was modified, added, or deleted
910* `--stat` โ Show a histogram of the changes
911* `--types` โ For each path, show only its type before and after
···24612462Split a revision in two
24632464-Starts a [diff editor] on the changes in the revision. Edit the right side of the diff until it has the content you want in the new revision. Once you close the editor, your edited content will replace the previous revision. The remaining changes will be put in a new revision on top.
24652466[diff editor]: https://jj-vcs.github.io/jj/latest/config/#editing-diffs
2467···2524* `-i`, `--interactive` โ Interactively choose which parts to squash
2525* `--tool <NAME>` โ Specify diff editor to be used (implies --interactive)
2526* `-k`, `--keep-emptied` โ The source revision will not be abandoned
00000252725282529
···906* `-p`, `--patch` โ Show patch compared to the previous version of this change
907908 If the previous version has different parents, it will be temporarily rebased to the parents of the new version, so the diff is not contaminated by unrelated changes.
909+* `--diff-snapshots` โ Changes the behavior of `--patch`, `--git`, etc to show diffs from rebases
910+911+ Implies `--patch` if no other diff format is requested.
912+913+ Normally, `jj evolog -p` shows a so-called "interdiff", temporarily rebasing the versions of a revision to the same parents, in order to omit differences in the file contents that are caused by rebases.
914+915+ This option disables this behavior, and shows diffs between the contents of the different versions without modification (as snapshots).
916+917+ Sometimes, `--diff-snapshots` can show fewer differences to be shown. For example, let's say the current revision is not empty and we perform `jj squash --keep-empty -r @` to make it empty. Then, `jj evolog -p --diff-snapshots` will not show any changes since the contents of the files in the current revision did not change. However, `jj evolog -p` will show a change, representing the fact that a non-empty revision became empty.
918* `-s`, `--summary` โ For each path, show only whether it was modified, added, or deleted
919* `--stat` โ Show a histogram of the changes
920* `--types` โ For each path, show only its type before and after
···24702471Split a revision in two
24722473+Starts a [diff editor] on the changes in the revision. Edit the right side of the diff until it has the content you want in the new revision. Once you close the editor, your edited content will be put in a new revision before the original revision, while the remaining changes will replace the original revision.
24742475[diff editor]: https://jj-vcs.github.io/jj/latest/config/#editing-diffs
2476···2533* `-i`, `--interactive` โ Interactively choose which parts to squash
2534* `--tool <NAME>` โ Specify diff editor to be used (implies --interactive)
2535* `-k`, `--keep-emptied` โ The source revision will not be abandoned
2536+* `--restore-descendants` โ Preserve the content (not the diff) when rebasing descendants of the source and target commits
2537+2538+ Only the snapshots of the `--from` and the `--into` commits will be modified.
2539+2540+ 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.
254125422543
···971 insta::assert_snapshot!(output, @r"
972 Make sure I can pick this up
973 [EOF]
974- ------- stderr -------
975- Warning: Deprecated configuration file `$TEST_ENV/home/Library/Application Support/jj/config.toml`.
976- Configuration files in `~/Library/Application Support/jj` are deprecated, and support will be removed in a future release.
977- Instead, move your configuration files to `~/.config/jj`.
978- [EOF]
979- ");
980-981- // if XDG_CONFIG_HOME is ~/Library/Application Support,
982- // you shouldn't get a warning
983- let output = test_env.run_jj_with(|cmd| {
984- cmd.env_remove("JJ_CONFIG")
985- .env(
986- "XDG_CONFIG_HOME",
987- test_env.home_dir().join("Library/Application Support"),
988- )
989- .args(["config", "get", "foo.bar"])
990- });
991- insta::assert_snapshot!(output, @r"
992- Make sure I can pick this up
993- [EOF]
994 ");
995996 // if you set JJ_CONFIG, you shouldn't get a warning
···971 insta::assert_snapshot!(output, @r"
972 Make sure I can pick this up
973 [EOF]
00000000000000000000974 ");
975976 // if you set JJ_CONFIG, you shouldn't get a warning
+2-2
cli/tests/test_describe_command.rs
···618 let output = work_dir.run_jj(["describe"]);
619 insta::assert_snapshot!(output, @r#"
620 ------- stderr -------
621- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
622 Working copy (@) now at: qpvuntsm 7276dfff TESTED=TODO
623 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
624 [EOF]
···639 let output = work_dir.run_jj(["describe", "--no-edit", "--reset-author"]);
640 insta::assert_snapshot!(output, @r#"
641 ------- stderr -------
642- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
643 Working copy (@) now at: kkmpptxz 7118bcb8 (empty) (no description set)
644 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
645 [EOF]
···618 let output = work_dir.run_jj(["describe"]);
619 insta::assert_snapshot!(output, @r#"
620 ------- stderr -------
621+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
622 Working copy (@) now at: qpvuntsm 7276dfff TESTED=TODO
623 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
624 [EOF]
···639 let output = work_dir.run_jj(["describe", "--no-edit", "--reset-author"]);
640 insta::assert_snapshot!(output, @r#"
641 ------- stderr -------
642+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
643 Working copy (@) now at: kkmpptxz 7118bcb8 (empty) (no description set)
644 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
645 [EOF]
+3-3
cli/tests/test_diff_command.rs
···253 -4
254 [EOF]
255 ------- stderr -------
256- Warning: Deprecated CLI-provided config: ui.diff.format is updated to ui.diff-formatter = ":git"
257 [EOF]
258 "#);
259···3138 file3
3139 [EOF]
3140 ------- stderr -------
3141- Warning: Deprecated CLI-provided config: ui.diff.tool is renamed to ui.diff-formatter
3142- Warning: Deprecated CLI-provided config: ui.diff.format is deleted (superseded by ui.diff-formatter)
3143 [EOF]
3144 ");
3145
···253 -4
254 [EOF]
255 ------- stderr -------
256+ Warning: Deprecated config: ui.diff.format is updated to ui.diff-formatter = ":git"
257 [EOF]
258 "#);
259···3138 file3
3139 [EOF]
3140 ------- stderr -------
3141+ Warning: Deprecated config: ui.diff.tool is renamed to ui.diff-formatter
3142+ Warning: Deprecated config: ui.diff.format is deleted (superseded by ui.diff-formatter)
3143 [EOF]
3144 ");
3145
···171 -- operation e0f8e58b3800 (2001-02-03 08:05:08) new empty commit
172 [EOF]
173 ");
174+175+ // With `--diff-snapshots`, the rebase does show a diff
176+ // TODO: Bug, not implemented yet with --no-graph
177+ let output = work_dir.run_jj(["evolog", "--no-graph", "--git", "--diff-snapshots"]);
178+ insta::assert_snapshot!(output, @r"
179+ rlvkpnrz test.user@example.com 2001-02-03 08:05:10 33c10ace
180+ my description
181+ -- operation 3499115d3831 (2001-02-03 08:05:10) snapshot working copy
182+ diff --git a/file1 b/file1
183+ index 0000000000..2ab19ae607 100644
184+ --- a/file1
185+ +++ b/file1
186+ @@ -1,7 +1,1 @@
187+ -<<<<<<< Conflict 1 of 1
188+ -%%%%%%% Changes from base to side #1
189+ --foo
190+ -+++++++ Contents of side #2
191+ -foo
192+ -bar
193+ ->>>>>>> Conflict 1 of 1 ends
194+ +resolved
195+ rlvkpnrz hidden test.user@example.com 2001-02-03 08:05:09 7f56b2a0 conflict
196+ my description
197+ -- operation eb87ec366530 (2001-02-03 08:05:09) rebase commit 51e08f95160c897080d035d330aead3ee6ed5588
198+ rlvkpnrz hidden test.user@example.com 2001-02-03 08:05:09 51e08f95
199+ my description
200+ -- operation 18a971ce330a (2001-02-03 08:05:09) snapshot working copy
201+ diff --git a/file1 b/file1
202+ index 257cc5642c..3bd1f0e297 100644
203+ --- a/file1
204+ +++ b/file1
205+ @@ -1,1 +1,2 @@
206+ foo
207+ +bar
208+ diff --git a/file2 b/file2
209+ new file mode 100644
210+ index 0000000000..257cc5642c
211+ --- /dev/null
212+ +++ b/file2
213+ @@ -0,0 +1,1 @@
214+ +foo
215+ rlvkpnrz hidden test.user@example.com 2001-02-03 08:05:08 b955b72e
216+ (empty) my description
217+ -- operation e0f8e58b3800 (2001-02-03 08:05:08) new empty commit
218+ [EOF]
219+ ");
220}
221222#[test]
+1-2
cli/tests/test_git_fetch.rs
···1038 ]);
1039 insta::assert_snapshot!(output, @r"
1040 ------- stderr -------
1041- Warning: No branch matching `noexist1` found on any specified/configured remote
1042- Warning: No branch matching `noexist2` found on any specified/configured remote
1043 Nothing changed.
1044 [EOF]
1045 ");
···1038 ]);
1039 insta::assert_snapshot!(output, @r"
1040 ------- stderr -------
1041+ Warning: No branch matching `noexist1`, `noexist2` found on any specified/configured remote
01042 Nothing changed.
1043 [EOF]
1044 ");
+140-140
cli/tests/test_split_command.rs
···73 let output = work_dir.run_jj(["split", "file2"]);
74 insta::assert_snapshot!(output, @r"
75 ------- stderr -------
76- Selected changes : qpvuntsm 6dbc7747 (no description set)
77- Remaining changes: zsuskuln 42cbbc02 (no description set)
78- Working copy (@) now at: zsuskuln 42cbbc02 (no description set)
79- Parent commit (@-) : qpvuntsm 6dbc7747 (no description set)
80 [EOF]
81 ");
82 insta::assert_snapshot!(
···92 assert!(!test_env.env_root().join("editor1").exists());
9394 insta::assert_snapshot!(get_log_output(&work_dir), @r"
95- @ zsuskulnrvyr false
96- โ qpvuntsmwlqt false
97 โ zzzzzzzzzzzz true
98 [EOF]
99 ");
···128 ------- stderr -------
129 Warning: All changes have been selected, so the original revision will become empty
130 Rebased 1 descendant commits
131- Selected changes : qpvuntsm 9fd1c9e1 (no description set)
132- Remaining changes: znkkpsqq 41e0da21 (empty) (no description set)
133- Working copy (@) now at: zsuskuln a06e40b8 (no description set)
134- Parent commit (@-) : znkkpsqq 41e0da21 (empty) (no description set)
135 [EOF]
136 ");
137138 insta::assert_snapshot!(get_log_output(&work_dir), @r"
139- @ zsuskulnrvyr false
140- โ znkkpsqqskkl true
141- โ qpvuntsmwlqt false
142 โ zzzzzzzzzzzz true
143 [EOF]
144 ");
···159 ------- stderr -------
160 Warning: No changes have been selected, so the new revision will be empty
161 Rebased 1 descendant commits
162- Selected changes : qpvuntsm 49416632 (empty) (no description set)
163- Remaining changes: lylxulpl 718afbf5 (no description set)
164- Working copy (@) now at: zsuskuln 0ed53ee6 (no description set)
165- Parent commit (@-) : lylxulpl 718afbf5 (no description set)
166 [EOF]
167 ");
168169 insta::assert_snapshot!(get_log_output(&work_dir), @r"
170- @ zsuskulnrvyr false
171- โ lylxulplsnyw false
172- โ qpvuntsmwlqt true
173 โ zzzzzzzzzzzz true
174 [EOF]
175 ");
···207 let output = work_dir.run_jj(["split", "file1"]);
208 insta::assert_snapshot!(output, @r#"
209 ------- stderr -------
210- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
211- Selected changes : qpvuntsm c7f7b14b part 1
212- Remaining changes: kkmpptxz ac33a5a9 part 2
213- Working copy (@) now at: kkmpptxz ac33a5a9 part 2
214- Parent commit (@-) : qpvuntsm c7f7b14b part 1
215 [EOF]
216 "#);
217···236 JJ: Lines starting with "JJ:" (like this one) will be removed.
237 "#);
238 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
239- @ kkmpptxzrspx false part 2
240- โ qpvuntsmwlqt false part 1
241 โ zzzzzzzzzzzz true
242 [EOF]
243 ------- stderr -------
244- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
245 [EOF]
246 "#);
247}
···265 let output = work_dir.run_jj(["split", "file1"]);
266 insta::assert_snapshot!(output, @r#"
267 ------- stderr -------
268- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
269- Selected changes : qpvuntsm ff633dcc TESTED=TODO
270- Remaining changes: rlvkpnrz b1d20b7e (no description set)
271- Working copy (@) now at: rlvkpnrz b1d20b7e (no description set)
272- Parent commit (@-) : qpvuntsm ff633dcc TESTED=TODO
273 [EOF]
274 "#);
275···291 "#);
292 assert!(!test_env.env_root().join("editor2").exists());
293 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
294- @ rlvkpnrzqnoo false
295- โ qpvuntsmwlqt false TESTED=TODO
296 โ zzzzzzzzzzzz true
297 [EOF]
298 ------- stderr -------
299- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
300 [EOF]
301 "#);
302}
···346 insta::assert_snapshot!(output, @r"
347 ------- stderr -------
348 Rebased 2 descendant commits
349- Selected changes : qpvuntsm 74306e35 Add file1
350- Remaining changes: royxmykx 0a37745e Add file2
351- Working copy (@) now at: kkmpptxz 7ee84812 Add file4
352- Parent commit (@-) : rlvkpnrz d335bd94 Add file3
353 [EOF]
354 ");
355- insta::assert_snapshot!(get_log_output(&work_dir), @r###"
356 @ kkmpptxzrspx false Add file4
357 โ rlvkpnrzqnoo false Add file3
358- โ royxmykxtrkr false Add file2
359- โ qpvuntsmwlqt false Add file1
360 โ zzzzzzzzzzzz true
361 [EOF]
362- "###);
363364 // The commit we're splitting has a description, so the user will be
365 // prompted to enter a description for each of the commits.
···388 // - The initial empty commit.
389 // - The rewritten commit from the snapshot after the files were added.
390 // - The rewritten commit once the description is added during `jj commit`.
391- // - The rewritten commit after the split.
392- let evolog_1 = work_dir.run_jj(["evolog", "-r", "qpvun"]);
393 insta::assert_snapshot!(evolog_1, @r"
394- โ qpvuntsm test.user@example.com 2001-02-03 08:05:12 74306e35
395 โ Add file1
396- โ -- operation 994b490f285d (2001-02-03 08:05:12) split commit 1d2499e72cefc8a2b87ebb47569140857b96189f
397 โ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 1d2499e7
398 โ Add file1 & file2
399 โ -- operation adf4f33386c9 (2001-02-03 08:05:08) commit f5700f8ef89e290e4e90ae6adc0908707e0d8c85
···407 ");
408409 // The evolog for the second commit is the same, except that the change id
410- // changes after the split.
411- let evolog_2 = work_dir.run_jj(["evolog", "-r", "royxm"]);
412 insta::assert_snapshot!(evolog_2, @r"
413- โ royxmykx test.user@example.com 2001-02-03 08:05:12 0a37745e
414 โ Add file2
415- โ -- operation 994b490f285d (2001-02-03 08:05:12) split commit 1d2499e72cefc8a2b87ebb47569140857b96189f
416 โ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 1d2499e7
417 โ Add file1 & file2
418 โ -- operation adf4f33386c9 (2001-02-03 08:05:08) commit f5700f8ef89e290e4e90ae6adc0908707e0d8c85
···461 insta::assert_snapshot!(output, @r"
462 ------- stderr -------
463 Rebased 1 descendant commits
464- Selected changes : kkmpptxz cc199567 Add file1
465- Remaining changes: royxmykx e488409f Add file2
466- Working copy (@) now at: zsuskuln ace61421 (empty) 2
467 Parent commit (@-) : qpvuntsm 884fe9b9 (empty) 1
468- Parent commit (@-) : royxmykx e488409f Add file2
469 [EOF]
470 ");
471 insta::assert_snapshot!(get_log_output(&work_dir), @r"
472 @ zsuskulnrvyr true 2
473 โโโฎ
474- โ โ royxmykxtrkr false Add file2
475- โ โ kkmpptxzrspx false Add file1
476 โ โ qpvuntsmwlqt true 1
477 โโโฏ
478 โ zzzzzzzzzzzz true
···498 โ zzzzzzzzzzzz true
499 [EOF]
500 ------- stderr -------
501- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
502 [EOF]
503 "#);
504···510 let output = work_dir.run_jj(["split", "--parallel", "file1"]);
511 insta::assert_snapshot!(output, @r#"
512 ------- stderr -------
513- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
514- Selected changes : qpvuntsm 7bcd474c TESTED=TODO
515- Remaining changes: kkmpptxz 431886f6 (no description set)
516- Working copy (@) now at: kkmpptxz 431886f6 (no description set)
517 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
518 Added 0 files, modified 0 files, removed 1 files
519 [EOF]
520 "#);
521 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
522- @ kkmpptxzrspx false
523- โ โ qpvuntsmwlqt false TESTED=TODO
524 โโโฏ
525 โ zzzzzzzzzzzz true
526 [EOF]
527 ------- stderr -------
528- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
529 [EOF]
530 "#);
531···550 // Check the evolog for the first commit. It shows three entries:
551 // - The initial empty commit.
552 // - The rewritten commit from the snapshot after the files were added.
553- // - The rewritten commit after the split.
554- let evolog_1 = work_dir.run_jj(["evolog", "-r", "qpvun"]);
555 insta::assert_snapshot!(evolog_1, @r#"
556- โ qpvuntsm test.user@example.com 2001-02-03 08:05:09 7bcd474c
557 โ TESTED=TODO
558- โ -- operation 2b21c33e1596 (2001-02-03 08:05:09) split commit f5700f8ef89e290e4e90ae6adc0908707e0d8c85
559 โ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 f5700f8e
560 โ (no description set)
561 โ -- operation 1663cd1cc445 (2001-02-03 08:05:08) snapshot working copy
···564 -- operation 8f47435a3990 (2001-02-03 08:05:07) add workspace 'default'
565 [EOF]
566 ------- stderr -------
567- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
568 [EOF]
569 "#);
570571 // The evolog for the second commit is the same, except that the change id
572- // changes after the split.
573- let evolog_2 = work_dir.run_jj(["evolog", "-r", "kkmpp"]);
574 insta::assert_snapshot!(evolog_2, @r#"
575- @ kkmpptxz test.user@example.com 2001-02-03 08:05:09 431886f6
576 โ (no description set)
577- โ -- operation 2b21c33e1596 (2001-02-03 08:05:09) split commit f5700f8ef89e290e4e90ae6adc0908707e0d8c85
578 โ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 f5700f8e
579 โ (no description set)
580 โ -- operation 1663cd1cc445 (2001-02-03 08:05:08) snapshot working copy
···583 -- operation 8f47435a3990 (2001-02-03 08:05:07) add workspace 'default'
584 [EOF]
585 ------- stderr -------
586- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
587 [EOF]
588 "#);
589}
···637 insta::assert_snapshot!(output, @r"
638 ------- stderr -------
639 Rebased 2 descendant commits
640- Selected changes : qpvuntsm 18c85f56 Add file1
641- Remaining changes: vruxwmqv cbdfd9cf Add file2
642- Working copy (@) now at: vruxwmqv cbdfd9cf Add file2
643 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
644 Added 0 files, modified 0 files, removed 1 files
645 [EOF]
···648 โ kkmpptxzrspx false Add file4
649 โ rlvkpnrzqnoo false Add file3
650 โโโฎ
651- โ @ vruxwmqvtpmx false Add file2
652- โ โ qpvuntsmwlqt false Add file1
653 โโโฏ
654 โ zzzzzzzzzzzz true
655 [EOF]
···714 insta::assert_snapshot!(output, @r"
715 ------- stderr -------
716 Rebased 1 descendant commits
717- Selected changes : kkmpptxz cc199567 Add file1
718- Remaining changes: royxmykx 82a5c527 Add file2
719- Working copy (@) now at: zsuskuln b7cdcdec (empty) 2
720 Parent commit (@-) : qpvuntsm 884fe9b9 (empty) 1
721- Parent commit (@-) : kkmpptxz cc199567 Add file1
722- Parent commit (@-) : royxmykx 82a5c527 Add file2
723 [EOF]
724 ");
725 insta::assert_snapshot!(get_log_output(&work_dir), @r"
726 @ zsuskulnrvyr true 2
727 โโโฌโโฎ
728- โ โ โ royxmykxtrkr false Add file2
729- โ โ โ kkmpptxzrspx false Add file1
730 โ โโโฏ
731 โ โ qpvuntsmwlqt true 1
732 โโโฏ
···793 let output = work_dir.run_jj(["split"]);
794 insta::assert_snapshot!(output, @r"
795 ------- stderr -------
796- Selected changes : qpvuntsm c664a51b (no description set)
797- Remaining changes: rlvkpnrz 7e5d65b1 (no description set)
798- Working copy (@) now at: rlvkpnrz 7e5d65b1 (no description set)
799- Parent commit (@-) : qpvuntsm c664a51b (no description set)
800 [EOF]
801 ");
802···824825 let output = work_dir.run_jj(["log", "--summary"]);
826 insta::assert_snapshot!(output, @r"
827- @ rlvkpnrz test.user@example.com 2001-02-03 08:05:08 7e5d65b1
828 โ (no description set)
829 โ A file2
830- โ qpvuntsm test.user@example.com 2001-02-03 08:05:08 c664a51b
831 โ (no description set)
832 โ A file1
833 โ zzzzzzzz root() 00000000
···869 let output = work_dir.run_jj(["split", "-i", "file1", "file2"]);
870 insta::assert_snapshot!(output, @r"
871 ------- stderr -------
872- Selected changes : rlvkpnrz cdc9960a (no description set)
873- Remaining changes: kkmpptxz 7255f070 (no description set)
874- Working copy (@) now at: kkmpptxz 7255f070 (no description set)
875- Parent commit (@-) : rlvkpnrz cdc9960a (no description set)
876 [EOF]
877 ");
878···889890 let output = work_dir.run_jj(["log", "--summary"]);
891 insta::assert_snapshot!(output, @r"
892- @ kkmpptxz test.user@example.com 2001-02-03 08:05:09 7255f070
893 โ (no description set)
894 โ M file2
895 โ M file3
896- โ rlvkpnrz test.user@example.com 2001-02-03 08:05:09 cdc9960a
897 โ (no description set)
898 โ A file1
899 โ qpvuntsm test.user@example.com 2001-02-03 08:05:08 ff687a2f
···947 main_dir.run_jj(["split", "file2"]).success();
948 // The working copy for both workspaces will be the second split commit.
949 insta::assert_snapshot!(get_workspace_log_output(&main_dir), @r"
950- @ royxmykxtrkr default@ second@ second-commit
951- โ qpvuntsmwlqt first-commit
952 โ zzzzzzzzzzzz
953 [EOF]
954 ");
···962 .unwrap();
963 main_dir.run_jj(["split", "file2", "--parallel"]).success();
964 insta::assert_snapshot!(get_workspace_log_output(&main_dir), @r"
965- @ yostqsxwqrlt default@ second@ second-commit
966- โ โ qpvuntsmwlqt first-commit
967 โโโฏ
968 โ zzzzzzzzzzzz
969 [EOF]
···1006 main_dir.run_jj(["split", "file2"]).success();
1007 // Only the working copy commit for the default workspace changes.
1008 insta::assert_snapshot!(get_workspace_log_output(&main_dir), @r"
1009- @ mzvwutvlkqwt default@ second-commit
1010- โ qpvuntsmwlqt first-commit
1011 โ โ pmmvwywvzvvn second@
1012 โโโฏ
1013 โ zzzzzzzzzzzz
···1023 .unwrap();
1024 main_dir.run_jj(["split", "file2", "--parallel"]).success();
1025 insta::assert_snapshot!(get_workspace_log_output(&main_dir), @r"
1026- @ vruxwmqvtpmx default@ second-commit
1027- โ โ qpvuntsmwlqt first-commit
1028 โโโฏ
1029 โ โ pmmvwywvzvvn second@
1030 โโโฏ
···1064 let output = work_dir.run_jj(["split", "file1"]);
1065 insta::assert_snapshot!(output, @r#"
1066 ------- stderr -------
1067- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
1068- Selected changes : qpvuntsm c7f7b14b part 1
1069- Remaining changes: kkmpptxz ac33a5a9 part 2
1070- Working copy (@) now at: kkmpptxz ac33a5a9 part 2
1071- Parent commit (@-) : qpvuntsm c7f7b14b part 1
1072 [EOF]
1073 "#);
1074···1097 JJ: Lines starting with "JJ:" (like this one) will be removed.
1098 "#);
1099 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
1100- @ kkmpptxzrspx false part 2
1101- โ qpvuntsmwlqt false part 1
1102 โ zzzzzzzzzzzz true
1103 [EOF]
1104 ------- stderr -------
1105- Warning: Deprecated user-level config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
1106 [EOF]
1107 "#);
1108}
···1120 let output = work_dir.run_jj(["split", "-m", "fix in file1", "file1"]);
1121 insta::assert_snapshot!(output, @r"
1122 ------- stderr -------
1123- Selected changes : qpvuntsm f2a70519 fix in file1
1124- Remaining changes: kkmpptxz cac11766 my feature
1125- Working copy (@) now at: kkmpptxz cac11766 my feature
1126- Parent commit (@-) : qpvuntsm f2a70519 fix in file1
1127 [EOF]
1128 ");
11291130 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1131- @ kkmpptxzrspx false my feature
1132- โ qpvuntsmwlqt false fix in file1
1133 โ zzzzzzzzzzzz true
1134 [EOF]
1135 ");
···1146 ]);
1147 insta::assert_snapshot!(output, @r"
1148 ------- stderr -------
1149- Selected changes : qpvuntsm d01cf12d fix in file1
1150- Remaining changes: royxmykx b1556ed9 my feature
1151- Working copy (@) now at: royxmykx b1556ed9 my feature
1152- Parent commit (@-) : qpvuntsm d01cf12d fix in file1
1153 [EOF]
1154 ");
11551156 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1157- @ royxmykxtrkr false my feature
1158- โ qpvuntsmwlqt false fix in file1
1159 โ
1160 โ CC: test.user@example.com
1161 โ zzzzzzzzzzzz true
···1470 .unwrap();
1471 let output = main_dir.run_jj(["split", "file2"]);
1472 match bookmark_behavior {
1473- BookmarkBehavior::LeaveBookmarkWithTarget => {
1474 insta::allow_duplicates! {
1475 insta::assert_snapshot!(output, @r#"
1476 ------- stderr -------
1477- Selected changes : qpvuntsm a481fe8a "*le-signet*" | first-commit
1478- Remaining changes: mzvwutvl 5f597a6e second-commit
1479- Working copy (@) now at: mzvwutvl 5f597a6e second-commit
1480- Parent commit (@-) : qpvuntsm a481fe8a "*le-signet*" | first-commit
1481 [EOF]
1482 "#);
1483 }
1484 insta::allow_duplicates! {
1485 insta::assert_snapshot!(get_log_output(&main_dir), @r#"
1486- @ mzvwutvlkqwt false second-commit
1487- โ qpvuntsmwlqt false "*le-signet*" first-commit
1488 โ zzzzzzzzzzzz true
1489 [EOF]
1490 "#);
1491 }
1492 }
1493- BookmarkBehavior::Default | BookmarkBehavior::MoveBookmarkToChild => {
1494 insta::allow_duplicates! {
1495 insta::assert_snapshot!(output, @r#"
1496 ------- stderr -------
···1521 .unwrap();
1522 main_dir.run_jj(["split", "file2", "--parallel"]).success();
1523 match bookmark_behavior {
1524- BookmarkBehavior::LeaveBookmarkWithTarget => {
1525 insta::allow_duplicates! {
1526 insta::assert_snapshot!(get_log_output(&main_dir), @r#"
1527- @ vruxwmqvtpmx false second-commit
1528- โ โ qpvuntsmwlqt false "*le-signet*" first-commit
1529 โโโฏ
1530 โ zzzzzzzzzzzz true
1531 [EOF]
1532 "#);
1533 }
1534 }
1535- BookmarkBehavior::Default | BookmarkBehavior::MoveBookmarkToChild => {
1536 insta::allow_duplicates! {
1537 insta::assert_snapshot!(get_log_output(&main_dir), @r#"
1538 @ vruxwmqvtpmx false "*le-signet*" second-commit
···73 let output = work_dir.run_jj(["split", "file2"]);
74 insta::assert_snapshot!(output, @r"
75 ------- stderr -------
76+ Selected changes : zsuskuln 8a73f71d (no description set)
77+ Remaining changes: qpvuntsm c4d8ebac (no description set)
78+ Working copy (@) now at: qpvuntsm c4d8ebac (no description set)
79+ Parent commit (@-) : zsuskuln 8a73f71d (no description set)
80 [EOF]
81 ");
82 insta::assert_snapshot!(
···92 assert!(!test_env.env_root().join("editor1").exists());
9394 insta::assert_snapshot!(get_log_output(&work_dir), @r"
95+ @ qpvuntsmwlqt false
96+ โ zsuskulnrvyr false
97 โ zzzzzzzzzzzz true
98 [EOF]
99 ");
···128 ------- stderr -------
129 Warning: All changes have been selected, so the original revision will become empty
130 Rebased 1 descendant commits
131+ Selected changes : znkkpsqq d6e65134 (no description set)
132+ Remaining changes: zsuskuln aa27eaa3 (empty) (no description set)
133+ Working copy (@) now at: qpvuntsm e94cab21 (no description set)
134+ Parent commit (@-) : zsuskuln aa27eaa3 (empty) (no description set)
135 [EOF]
136 ");
137138 insta::assert_snapshot!(get_log_output(&work_dir), @r"
139+ @ qpvuntsmwlqt false
140+ โ zsuskulnrvyr true
141+ โ znkkpsqqskkl false
142 โ zzzzzzzzzzzz true
143 [EOF]
144 ");
···159 ------- stderr -------
160 Warning: No changes have been selected, so the new revision will be empty
161 Rebased 1 descendant commits
162+ Selected changes : lylxulpl 3d639d71 (empty) (no description set)
163+ Remaining changes: znkkpsqq 706a0e77 (no description set)
164+ Working copy (@) now at: qpvuntsm 502cf440 (no description set)
165+ Parent commit (@-) : znkkpsqq 706a0e77 (no description set)
166 [EOF]
167 ");
168169 insta::assert_snapshot!(get_log_output(&work_dir), @r"
170+ @ qpvuntsmwlqt false
171+ โ znkkpsqqskkl false
172+ โ lylxulplsnyw true
173 โ zzzzzzzzzzzz true
174 [EOF]
175 ");
···207 let output = work_dir.run_jj(["split", "file1"]);
208 insta::assert_snapshot!(output, @r#"
209 ------- stderr -------
210+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
211+ Selected changes : kkmpptxz 530f78ed part 1
212+ Remaining changes: qpvuntsm 88189e08 part 2
213+ Working copy (@) now at: qpvuntsm 88189e08 part 2
214+ Parent commit (@-) : kkmpptxz 530f78ed part 1
215 [EOF]
216 "#);
217···236 JJ: Lines starting with "JJ:" (like this one) will be removed.
237 "#);
238 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
239+ @ qpvuntsmwlqt false part 2
240+ โ kkmpptxzrspx false part 1
241 โ zzzzzzzzzzzz true
242 [EOF]
243 ------- stderr -------
244+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
245 [EOF]
246 "#);
247}
···265 let output = work_dir.run_jj(["split", "file1"]);
266 insta::assert_snapshot!(output, @r#"
267 ------- stderr -------
268+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
269+ Selected changes : rlvkpnrz 16dc7e13 TESTED=TODO
270+ Remaining changes: qpvuntsm f40d53f2 (no description set)
271+ Working copy (@) now at: qpvuntsm f40d53f2 (no description set)
272+ Parent commit (@-) : rlvkpnrz 16dc7e13 TESTED=TODO
273 [EOF]
274 "#);
275···291 "#);
292 assert!(!test_env.env_root().join("editor2").exists());
293 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
294+ @ qpvuntsmwlqt false
295+ โ rlvkpnrzqnoo false TESTED=TODO
296 โ zzzzzzzzzzzz true
297 [EOF]
298 ------- stderr -------
299+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
300 [EOF]
301 "#);
302}
···346 insta::assert_snapshot!(output, @r"
347 ------- stderr -------
348 Rebased 2 descendant commits
349+ Selected changes : royxmykx e13e94b9 Add file1
350+ Remaining changes: qpvuntsm cf8ebbab Add file2
351+ Working copy (@) now at: kkmpptxz 73a16519 Add file4
352+ Parent commit (@-) : rlvkpnrz ec4d3a14 Add file3
353 [EOF]
354 ");
355+ insta::assert_snapshot!(get_log_output(&work_dir), @r"
356 @ kkmpptxzrspx false Add file4
357 โ rlvkpnrzqnoo false Add file3
358+ โ qpvuntsmwlqt false Add file2
359+ โ royxmykxtrkr false Add file1
360 โ zzzzzzzzzzzz true
361 [EOF]
362+ ");
363364 // The commit we're splitting has a description, so the user will be
365 // prompted to enter a description for each of the commits.
···388 // - The initial empty commit.
389 // - The rewritten commit from the snapshot after the files were added.
390 // - The rewritten commit once the description is added during `jj commit`.
391+ // - The rewritten commit after the split with a new change ID.
392+ let evolog_1 = work_dir.run_jj(["evolog", "-r", "royxm"]);
393 insta::assert_snapshot!(evolog_1, @r"
394+ โ royxmykx test.user@example.com 2001-02-03 08:05:12 e13e94b9
395 โ Add file1
396+ โ -- operation a8006fdd66fd (2001-02-03 08:05:12) split commit 1d2499e72cefc8a2b87ebb47569140857b96189f
397 โ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 1d2499e7
398 โ Add file1 & file2
399 โ -- operation adf4f33386c9 (2001-02-03 08:05:08) commit f5700f8ef89e290e4e90ae6adc0908707e0d8c85
···407 ");
408409 // The evolog for the second commit is the same, except that the change id
410+ // doesn't change after the split.
411+ let evolog_2 = work_dir.run_jj(["evolog", "-r", "qpvun"]);
412 insta::assert_snapshot!(evolog_2, @r"
413+ โ qpvuntsm test.user@example.com 2001-02-03 08:05:12 cf8ebbab
414 โ Add file2
415+ โ -- operation a8006fdd66fd (2001-02-03 08:05:12) split commit 1d2499e72cefc8a2b87ebb47569140857b96189f
416 โ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 1d2499e7
417 โ Add file1 & file2
418 โ -- operation adf4f33386c9 (2001-02-03 08:05:08) commit f5700f8ef89e290e4e90ae6adc0908707e0d8c85
···461 insta::assert_snapshot!(output, @r"
462 ------- stderr -------
463 Rebased 1 descendant commits
464+ Selected changes : royxmykx ad21dad2 Add file1
465+ Remaining changes: kkmpptxz 0922bd25 Add file2
466+ Working copy (@) now at: zsuskuln f59cd990 (empty) 2
467 Parent commit (@-) : qpvuntsm 884fe9b9 (empty) 1
468+ Parent commit (@-) : kkmpptxz 0922bd25 Add file2
469 [EOF]
470 ");
471 insta::assert_snapshot!(get_log_output(&work_dir), @r"
472 @ zsuskulnrvyr true 2
473 โโโฎ
474+ โ โ kkmpptxzrspx false Add file2
475+ โ โ royxmykxtrkr false Add file1
476 โ โ qpvuntsmwlqt true 1
477 โโโฏ
478 โ zzzzzzzzzzzz true
···498 โ zzzzzzzzzzzz true
499 [EOF]
500 ------- stderr -------
501+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
502 [EOF]
503 "#);
504···510 let output = work_dir.run_jj(["split", "--parallel", "file1"]);
511 insta::assert_snapshot!(output, @r#"
512 ------- stderr -------
513+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
514+ Selected changes : kkmpptxz bd9b3db1 TESTED=TODO
515+ Remaining changes: qpvuntsm 5597b805 (no description set)
516+ Working copy (@) now at: qpvuntsm 5597b805 (no description set)
517 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
518 Added 0 files, modified 0 files, removed 1 files
519 [EOF]
520 "#);
521 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
522+ @ qpvuntsmwlqt false
523+ โ โ kkmpptxzrspx false TESTED=TODO
524 โโโฏ
525 โ zzzzzzzzzzzz true
526 [EOF]
527 ------- stderr -------
528+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
529 [EOF]
530 "#);
531···550 // Check the evolog for the first commit. It shows three entries:
551 // - The initial empty commit.
552 // - The rewritten commit from the snapshot after the files were added.
553+ // - The rewritten commit after the split with a new change ID.
554+ let evolog_1 = work_dir.run_jj(["evolog", "-r", "kkmpp"]);
555 insta::assert_snapshot!(evolog_1, @r#"
556+ โ kkmpptxz test.user@example.com 2001-02-03 08:05:09 bd9b3db1
557 โ TESTED=TODO
558+ โ -- operation 372a3799b434 (2001-02-03 08:05:09) split commit f5700f8ef89e290e4e90ae6adc0908707e0d8c85
559 โ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 f5700f8e
560 โ (no description set)
561 โ -- operation 1663cd1cc445 (2001-02-03 08:05:08) snapshot working copy
···564 -- operation 8f47435a3990 (2001-02-03 08:05:07) add workspace 'default'
565 [EOF]
566 ------- stderr -------
567+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
568 [EOF]
569 "#);
570571 // The evolog for the second commit is the same, except that the change id
572+ // doesn't change after the split.
573+ let evolog_2 = work_dir.run_jj(["evolog", "-r", "qpvun"]);
574 insta::assert_snapshot!(evolog_2, @r#"
575+ @ qpvuntsm test.user@example.com 2001-02-03 08:05:09 5597b805
576 โ (no description set)
577+ โ -- operation 372a3799b434 (2001-02-03 08:05:09) split commit f5700f8ef89e290e4e90ae6adc0908707e0d8c85
578 โ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 f5700f8e
579 โ (no description set)
580 โ -- operation 1663cd1cc445 (2001-02-03 08:05:08) snapshot working copy
···583 -- operation 8f47435a3990 (2001-02-03 08:05:07) add workspace 'default'
584 [EOF]
585 ------- stderr -------
586+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
587 [EOF]
588 "#);
589}
···637 insta::assert_snapshot!(output, @r"
638 ------- stderr -------
639 Rebased 2 descendant commits
640+ Selected changes : vruxwmqv 3f0980cb Add file1
641+ Remaining changes: qpvuntsm dff79d19 Add file2
642+ Working copy (@) now at: qpvuntsm dff79d19 Add file2
643 Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
644 Added 0 files, modified 0 files, removed 1 files
645 [EOF]
···648 โ kkmpptxzrspx false Add file4
649 โ rlvkpnrzqnoo false Add file3
650 โโโฎ
651+ โ @ qpvuntsmwlqt false Add file2
652+ โ โ vruxwmqvtpmx false Add file1
653 โโโฏ
654 โ zzzzzzzzzzzz true
655 [EOF]
···714 insta::assert_snapshot!(output, @r"
715 ------- stderr -------
716 Rebased 1 descendant commits
717+ Selected changes : royxmykx ad21dad2 Add file1
718+ Remaining changes: kkmpptxz 23a2daac Add file2
719+ Working copy (@) now at: zsuskuln f1fcb7a6 (empty) 2
720 Parent commit (@-) : qpvuntsm 884fe9b9 (empty) 1
721+ Parent commit (@-) : royxmykx ad21dad2 Add file1
722+ Parent commit (@-) : kkmpptxz 23a2daac Add file2
723 [EOF]
724 ");
725 insta::assert_snapshot!(get_log_output(&work_dir), @r"
726 @ zsuskulnrvyr true 2
727 โโโฌโโฎ
728+ โ โ โ kkmpptxzrspx false Add file2
729+ โ โ โ royxmykxtrkr false Add file1
730 โ โโโฏ
731 โ โ qpvuntsmwlqt true 1
732 โโโฏ
···793 let output = work_dir.run_jj(["split"]);
794 insta::assert_snapshot!(output, @r"
795 ------- stderr -------
796+ Selected changes : rlvkpnrz 1ff7a783 (no description set)
797+ Remaining changes: qpvuntsm 429f292f (no description set)
798+ Working copy (@) now at: qpvuntsm 429f292f (no description set)
799+ Parent commit (@-) : rlvkpnrz 1ff7a783 (no description set)
800 [EOF]
801 ");
802···824825 let output = work_dir.run_jj(["log", "--summary"]);
826 insta::assert_snapshot!(output, @r"
827+ @ qpvuntsm test.user@example.com 2001-02-03 08:05:08 429f292f
828 โ (no description set)
829 โ A file2
830+ โ rlvkpnrz test.user@example.com 2001-02-03 08:05:08 1ff7a783
831 โ (no description set)
832 โ A file1
833 โ zzzzzzzz root() 00000000
···869 let output = work_dir.run_jj(["split", "-i", "file1", "file2"]);
870 insta::assert_snapshot!(output, @r"
871 ------- stderr -------
872+ Selected changes : kkmpptxz 0a5bea34 (no description set)
873+ Remaining changes: rlvkpnrz 7326e6fd (no description set)
874+ Working copy (@) now at: rlvkpnrz 7326e6fd (no description set)
875+ Parent commit (@-) : kkmpptxz 0a5bea34 (no description set)
876 [EOF]
877 ");
878···889890 let output = work_dir.run_jj(["log", "--summary"]);
891 insta::assert_snapshot!(output, @r"
892+ @ rlvkpnrz test.user@example.com 2001-02-03 08:05:09 7326e6fd
893 โ (no description set)
894 โ M file2
895 โ M file3
896+ โ kkmpptxz test.user@example.com 2001-02-03 08:05:09 0a5bea34
897 โ (no description set)
898 โ A file1
899 โ qpvuntsm test.user@example.com 2001-02-03 08:05:08 ff687a2f
···947 main_dir.run_jj(["split", "file2"]).success();
948 // The working copy for both workspaces will be the second split commit.
949 insta::assert_snapshot!(get_workspace_log_output(&main_dir), @r"
950+ @ qpvuntsmwlqt default@ second@ second-commit
951+ โ royxmykxtrkr first-commit
952 โ zzzzzzzzzzzz
953 [EOF]
954 ");
···962 .unwrap();
963 main_dir.run_jj(["split", "file2", "--parallel"]).success();
964 insta::assert_snapshot!(get_workspace_log_output(&main_dir), @r"
965+ @ qpvuntsmwlqt default@ second@ second-commit
966+ โ โ yostqsxwqrlt first-commit
967 โโโฏ
968 โ zzzzzzzzzzzz
969 [EOF]
···1006 main_dir.run_jj(["split", "file2"]).success();
1007 // Only the working copy commit for the default workspace changes.
1008 insta::assert_snapshot!(get_workspace_log_output(&main_dir), @r"
1009+ @ qpvuntsmwlqt default@ second-commit
1010+ โ mzvwutvlkqwt first-commit
1011 โ โ pmmvwywvzvvn second@
1012 โโโฏ
1013 โ zzzzzzzzzzzz
···1023 .unwrap();
1024 main_dir.run_jj(["split", "file2", "--parallel"]).success();
1025 insta::assert_snapshot!(get_workspace_log_output(&main_dir), @r"
1026+ @ qpvuntsmwlqt default@ second-commit
1027+ โ โ vruxwmqvtpmx first-commit
1028 โโโฏ
1029 โ โ pmmvwywvzvvn second@
1030 โโโฏ
···1064 let output = work_dir.run_jj(["split", "file1"]);
1065 insta::assert_snapshot!(output, @r#"
1066 ------- stderr -------
1067+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
1068+ Selected changes : kkmpptxz 530f78ed part 1
1069+ Remaining changes: qpvuntsm 88189e08 part 2
1070+ Working copy (@) now at: qpvuntsm 88189e08 part 2
1071+ Parent commit (@-) : kkmpptxz 530f78ed part 1
1072 [EOF]
1073 "#);
1074···1097 JJ: Lines starting with "JJ:" (like this one) will be removed.
1098 "#);
1099 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
1100+ @ qpvuntsmwlqt false part 2
1101+ โ kkmpptxzrspx false part 1
1102 โ zzzzzzzzzzzz true
1103 [EOF]
1104 ------- stderr -------
1105+ Warning: Deprecated config: ui.default-description is updated to template-aliases.default_commit_description = '"\n\nTESTED=TODO\n"'
1106 [EOF]
1107 "#);
1108}
···1120 let output = work_dir.run_jj(["split", "-m", "fix in file1", "file1"]);
1121 insta::assert_snapshot!(output, @r"
1122 ------- stderr -------
1123+ Selected changes : kkmpptxz b246503a fix in file1
1124+ Remaining changes: qpvuntsm e05b5012 my feature
1125+ Working copy (@) now at: qpvuntsm e05b5012 my feature
1126+ Parent commit (@-) : kkmpptxz b246503a fix in file1
1127 [EOF]
1128 ");
11291130 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1131+ @ qpvuntsmwlqt false my feature
1132+ โ kkmpptxzrspx false fix in file1
1133 โ zzzzzzzzzzzz true
1134 [EOF]
1135 ");
···1146 ]);
1147 insta::assert_snapshot!(output, @r"
1148 ------- stderr -------
1149+ Selected changes : royxmykx 87fbb488 fix in file1
1150+ Remaining changes: qpvuntsm fb598346 my feature
1151+ Working copy (@) now at: qpvuntsm fb598346 my feature
1152+ Parent commit (@-) : royxmykx 87fbb488 fix in file1
1153 [EOF]
1154 ");
11551156 insta::assert_snapshot!(get_log_output(&work_dir), @r"
1157+ @ qpvuntsmwlqt false my feature
1158+ โ royxmykxtrkr false fix in file1
1159 โ
1160 โ CC: test.user@example.com
1161 โ zzzzzzzzzzzz true
···1470 .unwrap();
1471 let output = main_dir.run_jj(["split", "file2"]);
1472 match bookmark_behavior {
1473+ BookmarkBehavior::Default | BookmarkBehavior::LeaveBookmarkWithTarget => {
1474 insta::allow_duplicates! {
1475 insta::assert_snapshot!(output, @r#"
1476 ------- stderr -------
1477+ Selected changes : mzvwutvl ac5cf500 first-commit
1478+ Remaining changes: qpvuntsm a13c536a "*le-signet*" | second-commit
1479+ Working copy (@) now at: qpvuntsm a13c536a "*le-signet*" | second-commit
1480+ Parent commit (@-) : mzvwutvl ac5cf500 first-commit
1481 [EOF]
1482 "#);
1483 }
1484 insta::allow_duplicates! {
1485 insta::assert_snapshot!(get_log_output(&main_dir), @r#"
1486+ @ qpvuntsmwlqt false "*le-signet*" second-commit
1487+ โ mzvwutvlkqwt false first-commit
1488 โ zzzzzzzzzzzz true
1489 [EOF]
1490 "#);
1491 }
1492 }
1493+ BookmarkBehavior::MoveBookmarkToChild => {
1494 insta::allow_duplicates! {
1495 insta::assert_snapshot!(output, @r#"
1496 ------- stderr -------
···1521 .unwrap();
1522 main_dir.run_jj(["split", "file2", "--parallel"]).success();
1523 match bookmark_behavior {
1524+ BookmarkBehavior::Default | BookmarkBehavior::LeaveBookmarkWithTarget => {
1525 insta::allow_duplicates! {
1526 insta::assert_snapshot!(get_log_output(&main_dir), @r#"
1527+ @ qpvuntsmwlqt false "*le-signet*" second-commit
1528+ โ โ vruxwmqvtpmx false first-commit
1529 โโโฏ
1530 โ zzzzzzzzzzzz true
1531 [EOF]
1532 "#);
1533 }
1534 }
1535+ BookmarkBehavior::MoveBookmarkToChild => {
1536 insta::allow_duplicates! {
1537 insta::assert_snapshot!(get_log_output(&main_dir), @r#"
1538 @ vruxwmqvtpmx false "*le-signet*" second-commit
···746}
747748#[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+1523+#[test]
1524fn test_squash_from_multiple() {
1525 let test_env = TestEnvironment::default();
1526 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
···27use crate::config::ConfigGetError;
28use crate::config::ConfigLayer;
29use crate::config::ConfigNamePathBuf;
30-use crate::config::ConfigSource;
31use crate::config::ConfigUpdateError;
32use crate::config::ConfigValue;
33use crate::config::StackedConfig;
···368pub fn migrate(
369 config: &mut StackedConfig,
370 rules: &[ConfigMigrationRule],
371-) -> Result<Vec<(ConfigSource, String)>, ConfigMigrateError> {
372 let mut descriptions = Vec::new();
373 for layer in config.layers_mut() {
374 migrate_layer(layer, rules, &mut descriptions)
···380fn migrate_layer(
381 layer: &mut Arc<ConfigLayer>,
382 rules: &[ConfigMigrationRule],
383- descriptions: &mut Vec<(ConfigSource, String)>,
384) -> Result<(), ConfigMigrateLayerError> {
385 let rules_to_apply = rules
386 .iter()
···392 let layer_mut = Arc::make_mut(layer);
393 for rule in rules_to_apply {
394 let desc = rule.apply(layer_mut)?;
395- descriptions.push((layer_mut.source, desc));
396 }
397 Ok(())
398}
···863 let descriptions = migrate(&mut config, &rules).unwrap();
864 insta::assert_debug_snapshot!(descriptions, @r#"
865 [
866- (
867- User,
868- "foo.old is renamed to foo.new",
869- ),
870- (
871- User,
872- "bar.old is deleted (superseded by baz.new)",
873- ),
874- (
875- User,
876- "bar.old is renamed to baz.new",
877- ),
878 ]
879 "#);
880 insta::assert_snapshot!(config.layers()[0].data, @r"
···923 let descriptions = migrate(&mut config, &rules).unwrap();
924 insta::assert_debug_snapshot!(descriptions, @r#"
925 [
926- (
927- User,
928- "foo.old is updated to foo.new = ['foo.old #0']",
929- ),
930- (
931- User,
932- "bar.old is deleted (superseded by baz.new)",
933- ),
934- (
935- User,
936- "bar.old is updated to baz.new = \"bar.old #1 updated\"",
937- ),
938 ]
939 "#);
940 insta::assert_snapshot!(config.layers()[0].data, @r"
···27use crate::config::ConfigGetError;
28use crate::config::ConfigLayer;
29use crate::config::ConfigNamePathBuf;
030use crate::config::ConfigUpdateError;
31use crate::config::ConfigValue;
32use crate::config::StackedConfig;
···367pub fn migrate(
368 config: &mut StackedConfig,
369 rules: &[ConfigMigrationRule],
370+) -> Result<Vec<String>, ConfigMigrateError> {
371 let mut descriptions = Vec::new();
372 for layer in config.layers_mut() {
373 migrate_layer(layer, rules, &mut descriptions)
···379fn migrate_layer(
380 layer: &mut Arc<ConfigLayer>,
381 rules: &[ConfigMigrationRule],
382+ descriptions: &mut Vec<String>,
383) -> Result<(), ConfigMigrateLayerError> {
384 let rules_to_apply = rules
385 .iter()
···391 let layer_mut = Arc::make_mut(layer);
392 for rule in rules_to_apply {
393 let desc = rule.apply(layer_mut)?;
394+ descriptions.push(desc);
395 }
396 Ok(())
397}
···862 let descriptions = migrate(&mut config, &rules).unwrap();
863 insta::assert_debug_snapshot!(descriptions, @r#"
864 [
865+ "foo.old is renamed to foo.new",
866+ "bar.old is deleted (superseded by baz.new)",
867+ "bar.old is renamed to baz.new",
000000000868 ]
869 "#);
870 insta::assert_snapshot!(config.layers()[0].data, @r"
···913 let descriptions = migrate(&mut config, &rules).unwrap();
914 insta::assert_debug_snapshot!(descriptions, @r#"
915 [
916+ "foo.old is updated to foo.new = ['foo.old #0']",
917+ "bar.old is deleted (superseded by baz.new)",
918+ "bar.old is updated to baz.new = \"bar.old #1 updated\"",
000000000919 ]
920 "#);
921 insta::assert_snapshot!(config.layers()[0].data, @r"
+26-40
lib/src/default_index/store.rs
···15#![allow(missing_docs)]
1617use std::any::Any;
18-use std::collections::HashMap;
19use std::fs;
20use std::io;
21use std::io::Write as _;
22use std::path::Path;
23use std::path::PathBuf;
24-use std::slice;
25use std::sync::Arc;
2627use itertools::Itertools as _;
···50use crate::object_id::ObjectId as _;
51use crate::op_store::OpStoreError;
52use crate::op_store::OperationId;
53-use crate::op_walk;
54use crate::operation::Operation;
55use crate::store::Store;
56···187 operation: &Operation,
188 store: &Arc<Store>,
189 ) -> Result<Arc<ReadonlyIndexSegment>, DefaultIndexStoreError> {
190- tracing::info!("scanning operations to index");
191 let operations_dir = self.operations_dir();
192 let commit_id_length = store.commit_id_length();
193 let change_id_length = store.change_id_length();
194- let ops_to_visit: Vec<_> =
195- op_walk::walk_ancestors(slice::from_ref(operation)).try_collect()?;
196- // Pick the latest existing ancestor operation as the parent segment.
197- let parent_op = ops_to_visit
198 .iter()
199- .find(|op| operations_dir.join(op.id().hex()).is_file())
200- .cloned();
201- // Remove ancestors of the latest existing operation, which should have
202- // been indexed in the parent segment. This could be optimized for
203- // linear history, but parent_op is often None.
204- let ops_to_visit = if let Some(op) = &parent_op {
205- let mut wanted_ops: HashMap<&OperationId, &Operation> =
206- ops_to_visit.iter().map(|op| (op.id(), op)).collect();
207- let mut work = vec![op.id()];
208- while let Some(id) = work.pop() {
209- if let Some(op) = wanted_ops.remove(id) {
210- work.extend(op.parent_ids());
211- }
00212 }
213- ops_to_visit
214- .iter()
215- .filter(|op| wanted_ops.contains_key(op.id()))
216- .cloned()
217- .collect()
218- } else {
219- ops_to_visit
220- };
221- tracing::info!(
222- ops_count = ops_to_visit.len(),
223- "collecting head commits to index"
224- );
225- let mut historical_heads: HashMap<CommitId, OperationId> = HashMap::new();
226- for op in &ops_to_visit {
227 for commit_id in op.view()?.all_referenced_commit_ids() {
228- if !historical_heads.contains_key(commit_id) {
229- historical_heads.insert(commit_id.clone(), op.id().clone());
230 }
231 }
232 }
233 let maybe_parent_file;
234 let mut mutable_index;
235- match &parent_op {
236 None => {
237 maybe_parent_file = None;
238 mutable_index = DefaultMutableIndex::full(commit_id_length, change_id_length);
239 }
240- Some(op) => {
241 let parent_file = self.load_index_segments_at_operation(
242- op.id(),
243 commit_id_length,
244 change_id_length,
245 )?;
···15#![allow(missing_docs)]
1617use std::any::Any;
18+use std::collections::HashSet;
19use std::fs;
20use std::io;
21use std::io::Write as _;
22use std::path::Path;
23use std::path::PathBuf;
024use std::sync::Arc;
2526use itertools::Itertools as _;
···49use crate::object_id::ObjectId as _;
50use crate::op_store::OpStoreError;
51use crate::op_store::OperationId;
052use crate::operation::Operation;
53use crate::store::Store;
54···185 operation: &Operation,
186 store: &Arc<Store>,
187 ) -> Result<Arc<ReadonlyIndexSegment>, DefaultIndexStoreError> {
188+ let view = operation.view()?;
189 let operations_dir = self.operations_dir();
190 let commit_id_length = store.commit_id_length();
191 let change_id_length = store.change_id_length();
192+ let mut visited_heads: HashSet<CommitId> =
193+ view.all_referenced_commit_ids().cloned().collect();
194+ let mut historical_heads: Vec<(CommitId, OperationId)> = visited_heads
0195 .iter()
196+ .map(|commit_id| (commit_id.clone(), operation.id().clone()))
197+ .collect();
198+ let mut parent_op_id: Option<OperationId> = None;
199+ for op in dag_walk::dfs_ok(
200+ [Ok(operation.clone())],
201+ |op: &Operation| op.id().clone(),
202+ |op: &Operation| op.parents().collect_vec(),
203+ ) {
204+ let op = op?;
205+ // Pick the latest existing ancestor operation as the parent
206+ // segment. Perhaps, breadth-first search is more appropriate here,
207+ // but that wouldn't matter in practice as the operation log is
208+ // mostly linear.
209+ if parent_op_id.is_none() && operations_dir.join(op.id().hex()).is_file() {
210+ parent_op_id = Some(op.id().clone());
211 }
212+ // TODO: no need to walk ancestors of the parent_op_id operation
0000000000000213 for commit_id in op.view()?.all_referenced_commit_ids() {
214+ if visited_heads.insert(commit_id.clone()) {
215+ historical_heads.push((commit_id.clone(), op.id().clone()));
216 }
217 }
218 }
219 let maybe_parent_file;
220 let mut mutable_index;
221+ match parent_op_id {
222 None => {
223 maybe_parent_file = None;
224 mutable_index = DefaultMutableIndex::full(commit_id_length, change_id_length);
225 }
226+ Some(parent_op_id) => {
227 let parent_file = self.load_index_segments_at_operation(
228+ &parent_op_id,
229 commit_id_length,
230 change_id_length,
231 )?;
+19-4
lib/src/repo.rs
···1406 /// The content of those descendants will remain untouched.
1407 /// Returns the number of reparented descendants.
1408 pub fn reparent_descendants(&mut self) -> BackendResult<usize> {
0000000000000001409 let roots = self.parent_mapping.keys().cloned().collect_vec();
1410- let mut num_reparented = 0;
1411 self.transform_descendants(roots, |rewriter| {
1412 if rewriter.parents_changed() {
01413 let builder = rewriter.reparent();
1414- builder.write()?;
1415- num_reparented += 1;
1416 }
1417 Ok(())
1418 })?;
1419 self.parent_mapping.clear();
1420- Ok(num_reparented)
1421 }
14221423 pub fn set_wc_commit(
···1406 /// The content of those descendants will remain untouched.
1407 /// Returns the number of reparented descendants.
1408 pub fn reparent_descendants(&mut self) -> BackendResult<usize> {
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();
01425 self.transform_descendants(roots, |rewriter| {
1426 if rewriter.parents_changed() {
1427+ let old_commit = rewriter.old_commit().clone();
1428 let builder = rewriter.reparent();
1429+ let reparented_commit = builder.write()?;
1430+ progress(old_commit, reparented_commit);
1431 }
1432 Ok(())
1433 })?;
1434 self.parent_mapping.clear();
1435+ Ok(())
1436 }
14371438 pub fn set_wc_commit(
+29-11
lib/src/rewrite.rs
···1109 pub abandoned_commits: Vec<Commit>,
1110}
11110000001112/// Squash `sources` into `destination` and return a [`SquashedCommit`] for the
1113/// resulting commit. Caller is responsible for setting the description and
1114/// finishing the commit.
···1116 repo: &'repo mut MutableRepo,
1117 sources: &[CommitWithSelection],
1118 destination: &Commit,
1119- keep_emptied: bool,
0001120) -> BackendResult<Option<SquashedCommit<'repo>>> {
1121 struct SourceCommit<'a> {
1122 commit: &'a CommitWithSelection,
···1169 // rewritten sources. Otherwise it will likely already have the content
1170 // changes we're moving, so applying them will have no effect and the
1171 // 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- })?;
0000000001182 }
1183 // Apply the selected changes onto the destination
1184 let mut destination_tree = rewritten_destination.tree()?;
···1109 pub abandoned_commits: Vec<Commit>,
1110}
11111112+#[derive(Clone, Debug)]
1113+pub struct SquashOptions {
1114+ pub keep_emptied: bool,
1115+ pub restore_descendants: bool,
1116+}
1117+1118/// Squash `sources` into `destination` and return a [`SquashedCommit`] for the
1119/// resulting commit. Caller is responsible for setting the description and
1120/// finishing the commit.
···1122 repo: &'repo mut MutableRepo,
1123 sources: &[CommitWithSelection],
1124 destination: &Commit,
1125+ SquashOptions {
1126+ keep_emptied,
1127+ restore_descendants,
1128+ }: SquashOptions,
1129) -> BackendResult<Option<SquashedCommit<'repo>>> {
1130 struct SourceCommit<'a> {
1131 commit: &'a CommitWithSelection,
···1178 // rewritten sources. Otherwise it will likely already have the content
1179 // changes we're moving, so applying them will have no effect and the
1180 // changes will disappear.
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+ }
1200 }
1201 // Apply the selected changes onto the destination
1202 let mut destination_tree = rewritten_destination.tree()?;