1#![warn(missing_docs)]
7#![warn(
8 clippy::all,
9 clippy::as_conversions,
10 clippy::clone_on_ref_ptr,
11 clippy::dbg_macro
12)]
13#![allow(clippy::too_many_arguments, clippy::blocks_in_conditions)]
14
15use std::collections::HashMap;
16use std::fmt::Write;
17use std::time::SystemTime;
18
19use eden_dag::Vertex;
20use lib::core::repo_ext::RepoExt;
21use lib::util::{ExitCode, EyreExitOr};
22use rayon::ThreadPoolBuilder;
23use tracing::instrument;
24
25use git_branchless_opts::{MoveOptions, ResolveRevsetOptions, Revset};
26use git_branchless_revset::resolve_commits;
27use lib::core::config::{
28 Hint, get_hint_enabled, get_hint_string, get_restack_preserve_timestamps,
29 print_hint_suppression_notice,
30};
31use lib::core::dag::{CommitSet, Dag, sorted_commit_set, union_all};
32use lib::core::effects::Effects;
33use lib::core::eventlog::{EventLogDb, EventReplayer};
34use lib::core::rewrite::{
35 BuildRebasePlanOptions, ExecuteRebasePlanOptions, ExecuteRebasePlanResult,
36 MergeConflictRemediation, RebasePlanBuilder, RebasePlanPermissions, RepoResource,
37 execute_rebase_plan,
38};
39use lib::git::{GitRunInfo, NonZeroOid, Repo};
40
41#[instrument]
42fn resolve_base_commit(
43 dag: &Dag,
44 merge_base_oid: Option<Vertex>,
45 oid: NonZeroOid,
46) -> eyre::Result<NonZeroOid> {
47 let bases = match merge_base_oid {
48 Some(merge_base_oid) => {
49 let range = dag.query_range(CommitSet::from(merge_base_oid), CommitSet::from(oid))?;
50 let roots = dag.query_roots(range.clone())?;
51 dag.query_children(roots)?.intersection(&range)
52 }
53 None => {
54 let ancestors = dag.query_ancestors(CommitSet::from(oid))?;
55 dag.query_roots(ancestors)?
56 }
57 };
58
59 match dag.set_first(&bases)? {
60 Some(base) => NonZeroOid::try_from(base),
61 None => Ok(oid),
62 }
63}
64
65#[instrument]
67pub fn r#move(
68 effects: &Effects,
69 git_run_info: &GitRunInfo,
70 sources: Vec<Revset>,
71 dest: Option<Revset>,
72 bases: Vec<Revset>,
73 exacts: Vec<Revset>,
74 resolve_revset_options: &ResolveRevsetOptions,
75 move_options: &MoveOptions,
76 fixup: bool,
77 insert: bool,
78 dry_run: bool,
79) -> EyreExitOr<()> {
80 let sources_provided = !sources.is_empty();
81 let bases_provided = !bases.is_empty();
82 let exacts_provided = !exacts.is_empty();
83 let dest_provided = dest.is_some();
84 let should_sources_default_to_head = !sources_provided && !bases_provided && !exacts_provided;
85
86 let repo = Repo::from_current_dir()?;
87 let head_oid = repo.get_head_info()?.oid;
88
89 let dest = match dest {
90 Some(dest) => dest,
91 None => match head_oid {
92 Some(oid) => Revset(oid.to_string()),
93 None => {
94 writeln!(
95 effects.get_output_stream(),
96 "No --dest argument was provided, and no OID for HEAD is available as a default"
97 )?;
98 return Ok(Err(ExitCode(1)));
99 }
100 },
101 };
102
103 let references_snapshot = repo.get_references_snapshot()?;
104 let conn = repo.get_db_conn()?;
105 let event_log_db = EventLogDb::new(&conn)?;
106 let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?;
107 let event_cursor = event_replayer.make_default_cursor();
108 let mut dag = Dag::open_and_sync(
109 effects,
110 &repo,
111 &event_replayer,
112 event_cursor,
113 &references_snapshot,
114 )?;
115
116 let source_oids: CommitSet =
117 match resolve_commits(effects, &repo, &mut dag, &sources, resolve_revset_options) {
118 Ok(commit_sets) => union_all(&commit_sets),
119 Err(err) => {
120 err.describe(effects)?;
121 return Ok(Err(ExitCode(1)));
122 }
123 };
124 let base_oids: CommitSet =
125 match resolve_commits(effects, &repo, &mut dag, &bases, resolve_revset_options) {
126 Ok(commit_sets) => union_all(&commit_sets),
127 Err(err) => {
128 err.describe(effects)?;
129 return Ok(Err(ExitCode(1)));
130 }
131 };
132 let exact_components = match resolve_commits(
133 effects,
134 &repo,
135 &mut dag,
136 &exacts,
137 resolve_revset_options,
138 ) {
139 Ok(commit_sets) => {
140 let exact_oids = union_all(&commit_sets);
141 let mut components: HashMap<NonZeroOid, CommitSet> = HashMap::new();
142
143 for component in dag.get_connected_components(&exact_oids)?.into_iter() {
144 let component_roots = dag.query_roots(component.clone())?;
145 let component_root = match dag.commit_set_to_vec(&component_roots)?.as_slice() {
146 [only_commit_oid] => *only_commit_oid,
147 _ => {
148 writeln!(
149 effects.get_error_stream(),
150 "The --exact flag can only be used to move ranges with exactly 1 root.\n\
151 Received range with {} roots: {:?}",
152 dag.set_count(&component_roots)?,
153 component_roots
154 )?;
155 return Ok(Err(ExitCode(1)));
156 }
157 };
158
159 let component_parents = dag.query_parents(CommitSet::from(component_root))?;
160 if dag.set_count(&component_parents)? != 1 {
161 writeln!(
162 effects.get_output_stream(),
163 "The --exact flag can only be used to move ranges or commits with exactly 1 parent.\n\
164 Received range with {} parents: {:?}",
165 dag.set_count(&component_parents)?,
166 component_parents
167 )?;
168 return Ok(Err(ExitCode(1)));
169 };
170
171 components.insert(component_root, component);
172 }
173
174 components
175 }
176 Err(err) => {
177 err.describe(effects)?;
178 return Ok(Err(ExitCode(1)));
179 }
180 };
181
182 let dest_oid: NonZeroOid = match resolve_commits(
183 effects,
184 &repo,
185 &mut dag,
186 std::slice::from_ref(&dest),
187 resolve_revset_options,
188 ) {
189 Ok(commit_sets) => match dag.commit_set_to_vec(&commit_sets[0])?.as_slice() {
190 [only_commit_oid] => *only_commit_oid,
191 other => {
192 let Revset(expr) = dest;
193 writeln!(
194 effects.get_error_stream(),
195 "Expected revset to expand to exactly 1 commit (got {}): {}",
196 other.len(),
197 expr,
198 )?;
199 return Ok(Err(ExitCode(1)));
200 }
201 },
202 Err(err) => {
203 err.describe(effects)?;
204 return Ok(Err(ExitCode(1)));
205 }
206 };
207
208 let base_oids = if should_sources_default_to_head {
209 match head_oid {
210 Some(head_oid) => CommitSet::from(head_oid),
211 None => {
212 writeln!(
213 effects.get_output_stream(),
214 "No --source or --base arguments were provided, and no OID for HEAD is available as a default"
215 )?;
216 return Ok(Err(ExitCode(1)));
217 }
218 }
219 } else {
220 base_oids
221 };
222 let base_oids = {
223 let mut result = Vec::new();
224 for base_oid in dag.commit_set_to_vec(&base_oids)? {
225 let merge_base_oid =
226 dag.query_gca_one(vec![base_oid, dest_oid].into_iter().collect::<CommitSet>())?;
227 let base_commit_oid = resolve_base_commit(&dag, merge_base_oid, base_oid)?;
228 result.push(CommitSet::from(base_commit_oid))
229 }
230 union_all(&result)
231 };
232 let source_oids = source_oids.union(&base_oids);
233
234 if let Some(head_oid) = head_oid {
235 if get_hint_enabled(&repo, Hint::MoveImplicitHeadArgument)? {
236 let should_warn_base = !sources_provided
237 && bases_provided
238 && dag.set_contains(&base_oids, head_oid)?
239 && dag.set_count(&base_oids)? == 1;
240 if should_warn_base {
241 writeln!(
242 effects.get_output_stream(),
243 "{}: you can omit the --base flag in this case, as it defaults to HEAD",
244 effects.get_glyphs().render(get_hint_string())?,
245 )?;
246 }
247
248 let should_warn_dest = dest_provided && dest_oid == head_oid;
249 if should_warn_dest {
250 writeln!(
251 effects.get_output_stream(),
252 "{}: you can omit the --dest flag in this case, as it defaults to HEAD",
253 effects.get_glyphs().render(get_hint_string())?,
254 )?;
255 }
256
257 if should_warn_base || should_warn_dest {
258 print_hint_suppression_notice(effects, Hint::MoveImplicitHeadArgument)?;
259 }
260 }
261 }
262 drop(base_oids);
263
264 let MoveOptions {
265 force_rewrite_public_commits,
266 force_in_memory,
267 force_on_disk,
268 detect_duplicate_commits_via_patch_id,
269 resolve_merge_conflicts,
270 dump_rebase_constraints,
271 dump_rebase_plan,
272 reparent,
273 } = *move_options;
274 let now = SystemTime::now();
275 let event_tx_id = event_log_db.make_transaction_id(now, "move")?;
276 let pool = ThreadPoolBuilder::new().build()?;
277 let repo_pool = RepoResource::new_pool(&repo)?;
278 let rebase_plan = {
279 let build_options = BuildRebasePlanOptions {
280 force_rewrite_public_commits,
281 dump_rebase_constraints,
282 dump_rebase_plan,
283 detect_duplicate_commits_via_patch_id,
284 };
285 let permissions = {
286 let commits_to_move = &source_oids;
287 let commits_to_move = commits_to_move.union(&union_all(
288 &exact_components.values().cloned().collect::<Vec<_>>(),
289 ));
290 let commits_to_move = if insert || fixup {
291 commits_to_move.union(&dag.query_children(CommitSet::from(dest_oid))?)
292 } else {
293 commits_to_move
294 };
295
296 match RebasePlanPermissions::verify_rewrite_set(&dag, build_options, &commits_to_move)?
297 {
298 Ok(permissions) => permissions,
299 Err(err) => {
300 err.describe(effects, &repo, &dag)?;
301 return Ok(Err(ExitCode(1)));
302 }
303 }
304 };
305 let mut builder = RebasePlanBuilder::new(&dag, permissions);
306
307 let source_roots = dag.query_roots(source_oids.clone())?;
308 for source_root in dag.commit_set_to_vec(&source_roots)? {
309 if fixup {
310 let commits = dag.query_descendants(CommitSet::from(source_root))?;
311 let commits = dag.commit_set_to_vec(&commits)?;
312 for commit in commits.iter() {
313 builder.fixup_commit(*commit, dest_oid)?;
314 }
315 } else {
316 builder.move_subtree(source_root, vec![dest_oid])?;
317 }
318
319 if reparent {
320 builder.reparent_commit(source_root, vec![dest_oid], &repo)?;
321 }
322 }
323
324 let component_roots: CommitSet = exact_components.keys().cloned().collect();
325 let component_roots: Vec<NonZeroOid> = sorted_commit_set(&repo, &dag, &component_roots)?
326 .iter()
327 .map(|commit| commit.get_oid())
328 .collect();
329 for component_root in component_roots.iter().cloned() {
330 let component = exact_components.get(&component_root).unwrap();
331
332 let mut possible_destinations: Vec<NonZeroOid> = vec![];
334 for root in component_roots.iter().cloned() {
335 let component = exact_components.get(&root).unwrap();
336 if !dag.set_contains(component, component_root)?
337 && dag.query_is_ancestor(root, component_root)?
338 {
339 possible_destinations.push(root);
340 }
341 }
342
343 let component_dest_oid = if possible_destinations.is_empty() {
344 dest_oid
345 } else {
346 for i in 1..possible_destinations.len() {
357 if !dag
358 .query_is_ancestor(possible_destinations[i - 1], possible_destinations[i])?
359 {
360 writeln!(
361 effects.get_output_stream(),
362 "This operation cannot be completed because the {} at {}\n\
363 has multiple possible parents also being moved. Please retry this operation\n\
364 without this {}, or with only 1 possible parent.",
365 if dag.set_count(component)? == 1 {
366 "commit"
367 } else {
368 "range of commits rooted"
369 },
370 component_root,
371 if dag.set_count(component)? == 1 {
372 "commit"
373 } else {
374 "range of commits"
375 },
376 )?;
377 return Ok(Err(ExitCode(1)));
378 }
379 }
380
381 let nearest_component = exact_components
382 .get(&possible_destinations[possible_destinations.len() - 1])
383 .unwrap();
384 let dest_ancestor = dag
387 .query_ancestors(CommitSet::from(component_root))?
388 .intersection(nearest_component);
389 match dag.set_first(&dag.query_heads(dest_ancestor.clone())?)? {
390 Some(head) => NonZeroOid::try_from(head)?,
391 None => dest_oid,
392 }
393 };
394
395 let component_parent = NonZeroOid::try_from(
397 dag.set_first(&dag.query_parents(CommitSet::from(component_root))?)?
398 .unwrap(),
399 )?;
400 let component_children: CommitSet =
401 dag.query_children(component.clone())?.difference(component);
402 let component_children = dag.filter_visible_commits(component_children)?;
403
404 for component_child in dag.commit_set_to_vec(&component_children)? {
405 if insert && dag.query_is_ancestor(component_child, component_dest_oid)? {
412 builder.move_range(component_child, component_dest_oid, component_parent)?;
413 } else {
414 builder.move_subtree(component_child, vec![component_parent])?;
415 }
416 }
417
418 if fixup {
419 let commits = dag.commit_set_to_vec(component)?;
420 for commit in commits.iter() {
421 builder.fixup_commit(*commit, dest_oid)?;
422 }
423 } else {
424 builder.move_subtree(component_root, vec![component_dest_oid])?;
425 }
426
427 if reparent {
428 builder.reparent_commit(component_root, vec![component_dest_oid], &repo)?;
429 }
430 }
431
432 if insert {
433 let source_head = {
434 let exact_head = if component_roots.is_empty() {
435 CommitSet::empty()
436 } else {
437 for i in 1..component_roots.len() {
441 if !dag.query_is_ancestor(component_roots[i - 1], component_roots[i])? {
442 writeln!(
443 effects.get_output_stream(),
444 "The --insert and --exact flags can only be used together when moving commits or\n\
445 ranges that form a single lineage, but {} is not an ancestor of {}.",
446 component_roots[i - 1],
447 component_roots[i]
448 )?;
449 return Ok(Err(ExitCode(1)));
450 }
451 }
452
453 let head_component = exact_components
454 .get(&component_roots[component_roots.len() - 1])
455 .unwrap()
456 .clone();
457 dag.query_heads(head_component)?
458 };
459 let source_heads: CommitSet = dag
460 .query_heads(dag.query_descendants(source_oids.clone())?)?
461 .union(&exact_head);
462 match dag.commit_set_to_vec(&source_heads)?.as_slice() {
463 [oid] => *oid,
464 _ => {
465 writeln!(
466 effects.get_output_stream(),
467 "The --insert flag cannot be used when moving subtrees or ranges with multiple heads."
468 )?;
469 return Ok(Err(ExitCode(1)));
470 }
471 }
472 };
473
474 let exact_components = exact_components
475 .values()
476 .cloned()
477 .collect::<Vec<CommitSet>>();
478 let exact_oids = union_all(&exact_components);
479 let dest_children: CommitSet = dag
481 .query_children(CommitSet::from(dest_oid))?
482 .difference(&source_oids)
483 .difference(&exact_oids);
484 let dest_children = dag.filter_visible_commits(dest_children)?;
485
486 for dest_child in dag.commit_set_to_vec(&dest_children)? {
487 builder.move_subtree(dest_child, vec![source_head])?;
488 if reparent {
489 builder.reparent_commit(dest_child, vec![source_head], &repo)?;
490 }
491 }
492 }
493 builder.build(effects, &pool, &repo_pool)?
494 };
495 let result = match rebase_plan {
496 Ok(None) => {
497 writeln!(effects.get_output_stream(), "Nothing to do.")?;
498 return Ok(Ok(()));
499 }
500 Ok(Some(rebase_plan)) => {
501 let options = ExecuteRebasePlanOptions {
502 now,
503 event_tx_id,
504 preserve_timestamps: get_restack_preserve_timestamps(&repo)?,
505 force_in_memory,
506 force_on_disk,
507 dry_run,
508 resolve_merge_conflicts,
509 check_out_commit_options: Default::default(),
510 };
511 execute_rebase_plan(
512 effects,
513 git_run_info,
514 &repo,
515 &event_log_db,
516 &rebase_plan,
517 &options,
518 )?
519 }
520 Err(err) => {
521 err.describe(effects, &repo, &dag)?;
522 return Ok(Err(ExitCode(1)));
523 }
524 };
525
526 match result {
527 ExecuteRebasePlanResult::Succeeded { rewritten_oids: _ } => Ok(Ok(())),
528
529 ExecuteRebasePlanResult::WouldSucceed if dry_run => {
530 writeln!(
531 effects.get_output_stream(),
532 "(This was a dry-run; no commits were moved. Re-run without --dry-run to actually move commits.)"
533 )?;
534 Ok(Ok(()))
535 }
536
537 ExecuteRebasePlanResult::WouldSucceed => {
538 unreachable!("WouldSucceed should only apply to dry runs")
539 }
540
541 ExecuteRebasePlanResult::DeclinedToMerge { failed_merge_info } => {
542 failed_merge_info.describe(effects, &repo, MergeConflictRemediation::Retry)?;
543 Ok(Err(ExitCode(1)))
544 }
545
546 ExecuteRebasePlanResult::Failed { exit_code } => Ok(Err(exit_code)),
547 }
548}