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