git_branchless_move/
lib.rs

1//! Move commits and subtrees from one place to another.
2//!
3//! Under the hood, this makes use of Git's advanced rebase functionality, which
4//! is also used to preserve merge commits using the `--rebase-merges` option.
5
6#![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/// Move a subtree from one place to another.
65#[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            // Find the non-inclusive ancestor components of the current root
320            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                // If there was a merge commit somewhere outside of the selected
334                // components, then it's possible that the current component
335                // could have multiple possible parents.
336                //
337                // To check for this, we can confirm that the nearest
338                // destination component is an ancestor of the previous (ie next
339                // nearest). This works because possible_destinations is made
340                // from component_roots, which has been sorted topologically; so
341                // each included component should "come after" the previous
342                // component.
343                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                // The current component could be descended from any commit
372                // in nearest_component, not just it's head.
373                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            // Again, we've already confirmed that each component has but 1 parent
383            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 the range being extracted has any child commits, then we
393                // need to move each of those subtrees up to the parent commit
394                // of the range. If, however, we're inserting the range and the
395                // destination commit is in one of those subtrees, then we
396                // should only move the commits from the root of that child
397                // subtree up to (and including) the destination commmit.
398                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                    // As long as component_roots has been sorted topologically,
421                    // we only need to compare adjacent elements to confirm a
422                    // single lineage.
423                    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            // Children of dest_oid that are not themselves being moved.
463            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}