diff --git a/git-branchless-lib/src/core/eventlog.rs b/git-branchless-lib/src/core/eventlog.rs index 77eeed9ce..d4a87a994 100644 --- a/git-branchless-lib/src/core/eventlog.rs +++ b/git-branchless-lib/src/core/eventlog.rs @@ -770,7 +770,7 @@ impl EventReplayer { ) -> eyre::Result { let (_effects, _progress) = effects.start_operation(OperationType::ProcessEvents); - let main_branch_reference_name = repo.get_main_branch_reference()?.get_name()?; + let main_branch_reference_name = repo.get_main_branch()?.get_reference_name()?; let mut result = EventReplayer::new(main_branch_reference_name); for event in event_log_db.get_events()? { result.process_event(&event); @@ -1209,7 +1209,7 @@ impl EventReplayer { cursor: EventCursor, repo: &Repo, ) -> eyre::Result { - let main_branch_reference_name = repo.get_main_branch_reference()?.get_name()?; + let main_branch_reference_name = repo.get_main_branch()?.get_reference_name()?; let main_branch_oid = self.get_cursor_branch_oid(cursor, &main_branch_reference_name)?; match main_branch_oid { Some(main_branch_oid) => Ok(main_branch_oid), diff --git a/git-branchless-lib/src/core/repo_ext.rs b/git-branchless-lib/src/core/repo_ext.rs index a835970f8..a4248ee66 100644 --- a/git-branchless-lib/src/core/repo_ext.rs +++ b/git-branchless-lib/src/core/repo_ext.rs @@ -6,7 +6,7 @@ use color_eyre::Help; use eyre::Context; use tracing::instrument; -use crate::git::{NonZeroOid, Reference, ReferenceName, Repo}; +use crate::git::{Branch, BranchType, NonZeroOid, ReferenceName, Repo}; use super::config::get_main_branch_name; @@ -25,8 +25,8 @@ pub struct RepoReferencesSnapshot { /// Helper functions on [`Repo`]. pub trait RepoExt { - /// Get the `Reference` for the main branch for the repository. - fn get_main_branch_reference(&self) -> eyre::Result; + /// Get the `Branch` for the main branch for the repository. + fn get_main_branch(&self) -> eyre::Result; /// Get the OID corresponding to the main branch. fn get_main_branch_oid(&self) -> eyre::Result; @@ -42,18 +42,13 @@ pub trait RepoExt { } impl RepoExt for Repo { - fn get_main_branch_reference(&self) -> eyre::Result { + fn get_main_branch(&self) -> eyre::Result { let main_branch_name = get_main_branch_name(self)?; - match self.find_branch(&main_branch_name, git2::BranchType::Local)? { - Some(branch) => match branch.get_upstream_branch()? { - Some(upstream_branch) => Ok(upstream_branch.into_reference()), - None => Ok(branch.into_reference()), - }, - None => match self.find_branch(&main_branch_name, git2::BranchType::Remote)? { - Some(branch) => Ok(branch.into_reference()), - None => { - let suggestion = format!( - r" + match self.find_branch(&main_branch_name, BranchType::Local)? { + Some(branch) => Ok(branch), + None => { + let suggestion = format!( + r" The main branch {:?} could not be found in your repository at path: {:?}. These branches exist: {:?} @@ -61,35 +56,34 @@ Either create it, or update the main branch setting by running: git branchless init --main-branch ", - get_main_branch_name(self)?, - self.get_path(), - self.get_all_local_branches()? - .into_iter() - .map(|branch| { - branch - .into_reference() - .get_name() - .map(|s| format!("{:?}", s)) - .wrap_err("converting branch to reference") - }) - .collect::>>()?, - ); - Err(eyre::eyre!("Could not find repository main branch") - .with_suggestion(|| suggestion)) - } - }, + get_main_branch_name(self)?, + self.get_path(), + self.get_all_local_branches()? + .into_iter() + .map(|branch| { + branch + .into_reference() + .get_name() + .map(|s| format!("{:?}", s)) + .wrap_err("converting branch to reference") + }) + .collect::>>()?, + ); + Err(eyre::eyre!("Could not find repository main branch") + .with_suggestion(|| suggestion)) + } } } #[instrument] fn get_main_branch_oid(&self) -> eyre::Result { - let main_branch_reference = self.get_main_branch_reference()?; - let commit = main_branch_reference.peel_to_commit()?; - match commit { - Some(commit) => Ok(commit.get_oid()), + let main_branch = self.get_main_branch()?; + let main_branch_oid = main_branch.get_oid()?; + match main_branch_oid { + Some(main_branch_oid) => Ok(main_branch_oid), None => eyre::bail!( "Could not find commit pointed to by main branch: {:?}", - main_branch_reference.get_name()? + main_branch.get_name()?, ), } } @@ -109,15 +103,6 @@ Either create it, or update the main branch setting by running: } } - // The main branch may be a remote branch, in which case it won't be - // returned in the iteration above. - let main_branch_name = self.get_main_branch_reference()?.get_name()?; - let main_branch_oid = self.get_main_branch_oid()?; - result - .entry(main_branch_oid) - .or_insert_with(HashSet::new) - .insert(main_branch_name); - Ok(result) } diff --git a/git-branchless-lib/src/core/rewrite/execute.rs b/git-branchless-lib/src/core/rewrite/execute.rs index 5bbc3c09d..3f4f793b9 100644 --- a/git-branchless-lib/src/core/rewrite/execute.rs +++ b/git-branchless-lib/src/core/rewrite/execute.rs @@ -30,6 +30,8 @@ pub fn move_branches<'a>( event_tx_id: EventTransactionId, rewritten_oids_map: &'a HashMap, ) -> eyre::Result<()> { + let main_branch = repo.get_main_branch()?; + let main_branch_name = main_branch.get_reference_name()?; let branch_oid_to_names = repo.get_branch_oid_to_names()?; // We may experience an error in the case of a branch move. Ideally, we @@ -78,22 +80,54 @@ pub fn move_branches<'a>( MaybeZeroOid::Zero => { for name in names { - match repo.find_reference(name) { - Ok(Some(mut reference)) => { - if let Err(err) = reference.delete() { + if name == &main_branch_name { + // Hack? Never delete the main branch. We probably got here by syncing the + // main branch with the upstream version, but all main branch commits were + // skipped. For a regular branch, we would delete the branch, but for the + // main branch, we should update it to point directly to the upstream + // version. + let target_oid = match main_branch.get_upstream_branch_target()? { + Some(target_oid) => { + if let Err(err) = repo.create_reference( + &main_branch_name, + target_oid, + true, + "move main branch", + ) { + branch_move_err = Some(eyre::eyre!(err)); + break 'outer; + } + MaybeZeroOid::NonZero(target_oid) + } + None => { + let mut main_branch_reference = + repo.get_main_branch()?.into_reference(); + if let Err(err) = main_branch_reference.delete() { + branch_move_err = Some(eyre::eyre!(err)); + break 'outer; + } + MaybeZeroOid::Zero + } + }; + branch_moves.push((*old_oid, target_oid, name)); + } else { + match repo.find_reference(name) { + Ok(Some(mut reference)) => { + if let Err(err) = reference.delete() { + branch_move_err = Some(eyre::eyre!(err)); + break 'outer; + } + } + Ok(None) => { + warn!(?name, "Reference not found, not deleting") + } + Err(err) => { branch_move_err = Some(eyre::eyre!(err)); break 'outer; } - } - Ok(None) => { - warn!(?name, "Reference not found, not deleting") - } - Err(err) => { - branch_move_err = Some(eyre::eyre!(err)); - break 'outer; - } - }; - branch_moves.push((*old_oid, MaybeZeroOid::Zero, name)); + }; + branch_moves.push((*old_oid, MaybeZeroOid::Zero, name)); + } } } } diff --git a/git-branchless-lib/src/core/rewrite/plan.rs b/git-branchless-lib/src/core/rewrite/plan.rs index a52dcfae7..55e6f3567 100644 --- a/git-branchless-lib/src/core/rewrite/plan.rs +++ b/git-branchless-lib/src/core/rewrite/plan.rs @@ -525,7 +525,7 @@ enum Constraint { } /// Options used to build a rebase plan. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct BuildRebasePlanOptions { /// Force rewriting public commits, even though other users may have access /// to those commits. diff --git a/git-branchless-lib/src/git/repo.rs b/git-branchless-lib/src/git/repo.rs index a9eb52775..0195a1721 100644 --- a/git-branchless-lib/src/git/repo.rs +++ b/git-branchless-lib/src/git/repo.rs @@ -2142,7 +2142,8 @@ impl<'repo> Branch<'repo> { Ok(self.inner.get().target().map(make_non_zero_oid)) } - /// Get the name of this branch, not including any `refs/heads/` prefix. + /// Get the name of this branch, not including any `refs/heads/` prefix. To get the full + /// reference name of this branch, instead call `.into_reference().get_name()?`. #[instrument] pub fn get_name(&self) -> eyre::Result<&str> { self.inner @@ -2150,6 +2151,18 @@ impl<'repo> Branch<'repo> { .ok_or_else(|| eyre::eyre!("Could not decode branch name")) } + /// Get the full reference name of this branch, including the `refs/heads/` or `refs/remotes/`x + /// prefix, as appropriate + #[instrument] + pub fn get_reference_name(&self) -> eyre::Result { + let reference_name = self + .inner + .get() + .name() + .ok_or_else(|| eyre::eyre!("Could not decode branch reference name"))?; + Ok(ReferenceName(reference_name.to_owned())) + } + /// If this branch tracks a remote ("upstream") branch, return that branch. #[instrument] pub fn get_upstream_branch(&self) -> Result>> { @@ -2171,6 +2184,18 @@ impl<'repo> Branch<'repo> { } } + /// If this branch tracks a remote ("upstream") branch, return the OID of the commit which that + /// branch points to. + #[instrument] + pub fn get_upstream_branch_target(&self) -> eyre::Result> { + let upstream_branch = match self.get_upstream_branch()? { + Some(upstream_branch) => upstream_branch, + None => return Ok(None), + }; + let target_oid = upstream_branch.get_oid()?; + Ok(target_oid) + } + /// Get the associated remote to push to for this branch. If there is no /// associated remote, returns `None`. Note that this never reads the value /// of `push.remoteDefault`. diff --git a/git-branchless/src/commands/bug_report.rs b/git-branchless/src/commands/bug_report.rs index e6c4f93c9..e2d14304d 100644 --- a/git-branchless/src/commands/bug_report.rs +++ b/git-branchless/src/commands/bug_report.rs @@ -187,7 +187,7 @@ fn collect_events(effects: &Effects, git_run_info: &GitRunInfo) -> eyre::Result< let redactor = Redactor::new({ let mut preserved_ref_names = HashSet::new(); - preserved_ref_names.insert(repo.get_main_branch_reference()?.get_name()?); + preserved_ref_names.insert(repo.get_main_branch()?.get_reference_name()?); preserved_ref_names }); diff --git a/git-branchless/src/commands/mod.rs b/git-branchless/src/commands/mod.rs index eb8d225af..e4ac0d24f 100644 --- a/git-branchless/src/commands/mod.rs +++ b/git-branchless/src/commands/mod.rs @@ -327,10 +327,10 @@ fn do_main_and_drop_locals() -> eyre::Result { } Command::Sync { - update_refs, + pull, move_options, revsets, - } => sync::sync(&effects, &git_run_info, update_refs, &move_options, revsets)?, + } => sync::sync(&effects, &git_run_info, pull, &move_options, revsets)?, Command::Undo { interactive, yes } => { undo::undo(&effects, &git_run_info, interactive, yes)? diff --git a/git-branchless/src/commands/restack.rs b/git-branchless/src/commands/restack.rs index 733c0a396..71c70bf1f 100644 --- a/git-branchless/src/commands/restack.rs +++ b/git-branchless/src/commands/restack.rs @@ -235,7 +235,7 @@ fn restack_branches( Some(branch_target) => branch_target, None => { warn!( - branch_name = ?branch.into_reference().get_name(), + branch_name = ?branch.get_reference_name()?, "Branch was not a direct reference, could not resolve target" ); continue; diff --git a/git-branchless/src/commands/submit.rs b/git-branchless/src/commands/submit.rs index 98659d4c2..65a570563 100644 --- a/git-branchless/src/commands/submit.rs +++ b/git-branchless/src/commands/submit.rs @@ -130,7 +130,7 @@ specified for `remote.pushDefault`, so cannot push these branches: {} Configure a value with: git config remote.pushDefault These remotes are available: {}", CategorizedReferenceName::new( - &repo.get_main_branch_reference()?.get_name()? + &repo.get_main_branch()?.get_reference_name()?, ) .friendly_describe(), branch_names.join(", "), @@ -183,8 +183,7 @@ To create and push them, retry this operation with the --create option." } fn get_default_remote(repo: &Repo) -> eyre::Result> { - let main_branch_reference = repo.get_main_branch_reference()?; - let main_branch_name = main_branch_reference.get_name()?; + let main_branch_name = repo.get_main_branch()?.get_reference_name()?; match CategorizedReferenceName::new(&main_branch_name) { name @ CategorizedReferenceName::LocalBranch { .. } => { if let Some(main_branch) = diff --git a/git-branchless/src/commands/sync.rs b/git-branchless/src/commands/sync.rs index 0a3411d36..d0702cd80 100644 --- a/git-branchless/src/commands/sync.rs +++ b/git-branchless/src/commands/sync.rs @@ -1,27 +1,31 @@ //! Implements the `git sync` command. +use cursive::theme::BaseColor; use std::fmt::Write; use std::time::SystemTime; use eden_dag::DagAlgorithm; +use eyre::Report; use itertools::Itertools; use lib::core::check_out::CheckOutCommitOptions; use lib::core::repo_ext::RepoExt; use lib::util::ExitCode; -use rayon::ThreadPoolBuilder; +use rayon::{ThreadPool, ThreadPoolBuilder}; use crate::opts::{MoveOptions, Revset}; use crate::revset::resolve_commits; use lib::core::config::get_restack_preserve_timestamps; -use lib::core::dag::{sorted_commit_set, union_all, CommitSet, Dag}; +use lib::core::dag::{commit_set_to_vec, sorted_commit_set, union_all, CommitSet, Dag}; use lib::core::effects::{Effects, OperationType}; use lib::core::eventlog::{EventLogDb, EventReplayer}; -use lib::core::formatting::{printable_styled_string, Glyphs, StyledStringBuilder}; +use lib::core::formatting::{printable_styled_string, StyledStringBuilder}; use lib::core::rewrite::{ execute_rebase_plan, BuildRebasePlanError, BuildRebasePlanOptions, ExecuteRebasePlanOptions, - ExecuteRebasePlanResult, RebasePlan, RebasePlanBuilder, RebasePlanPermissions, RepoResource, + ExecuteRebasePlanResult, RebasePlan, RebasePlanBuilder, RebasePlanPermissions, RepoPool, + RepoResource, }; -use lib::git::{Commit, GitRunInfo, NonZeroOid, Repo}; +use lib::core::task::ResourcePool; +use lib::git::{CategorizedReferenceName, Commit, GitRunInfo, NonZeroOid, Repo}; fn get_stack_roots(dag: &Dag) -> eyre::Result { let public_commits = dag.query_public_commits()?; @@ -45,18 +49,17 @@ fn get_stack_roots(dag: &Dag) -> eyre::Result { pub fn sync( effects: &Effects, git_run_info: &GitRunInfo, - update_refs: bool, + pull: bool, move_options: &MoveOptions, revsets: Vec, ) -> eyre::Result { - let glyphs = Glyphs::detect(); let repo = Repo::from_current_dir()?; let conn = repo.get_db_conn()?; let event_log_db = EventLogDb::new(&conn)?; let now = SystemTime::now(); let event_tx_id = event_log_db.make_transaction_id(now, "sync fetch")?; - if update_refs { + if pull { let exit_code = git_run_info.run(effects, Some(event_tx_id), &["fetch", "--all"])?; if !exit_code.is_success() { return Ok(exit_code); @@ -97,75 +100,12 @@ pub fn sync( dump_rebase_constraints, dump_rebase_plan, } = *move_options; - let pool = ThreadPoolBuilder::new().build()?; - let repo_pool = RepoResource::new_pool(&repo)?; - let root_commit_and_plans: Vec<(NonZeroOid, Option)> = { - let build_options = BuildRebasePlanOptions { - force_rewrite_public_commits, - detect_duplicate_commits_via_patch_id, - dump_rebase_constraints, - dump_rebase_plan, - }; - let permissions = match RebasePlanPermissions::verify_rewrite_set( - &dag, - &build_options, - &root_commit_oids, - )? { - Ok(permissions) => permissions, - Err(err) => { - err.describe(effects, &repo)?; - return Ok(ExitCode(1)); - } - }; - let builder = RebasePlanBuilder::new(&dag, permissions); - - let root_commit_oids = root_commits - .into_iter() - .map(|commit| commit.get_oid()) - .collect_vec(); - let root_commit_and_plans = pool.install(|| -> eyre::Result<_> { - let result = root_commit_oids - // Don't parallelize for now, since the status updates don't render well. - .into_iter() - .map( - |root_commit_oid| -> eyre::Result< - Result<(NonZeroOid, Option), BuildRebasePlanError>, - > { - // Keep access to the same underlying caches by cloning the same instance of the builder. - let mut builder = builder.clone(); - - let repo = repo_pool.try_create()?; - let root_commit = repo.find_commit_or_fail(root_commit_oid)?; - - let only_parent_id = - root_commit.get_only_parent().map(|parent| parent.get_oid()); - if only_parent_id == Some(references_snapshot.main_branch_oid) { - return Ok(Ok((root_commit_oid, None))); - } - - builder.move_subtree( - root_commit.get_oid(), - vec![references_snapshot.main_branch_oid], - )?; - let rebase_plan = builder.build(effects, &pool, &repo_pool)?; - Ok(rebase_plan.map(|rebase_plan| (root_commit_oid, rebase_plan))) - }, - ) - .collect::>>()? - .into_iter() - .collect::, BuildRebasePlanError>>(); - Ok(result) - })?; - - match root_commit_and_plans { - Ok(root_commit_and_plans) => root_commit_and_plans, - Err(err) => { - err.describe(effects, &repo)?; - return Ok(ExitCode(1)); - } - } + let build_options = BuildRebasePlanOptions { + force_rewrite_public_commits, + detect_duplicate_commits_via_patch_id, + dump_rebase_constraints, + dump_rebase_plan, }; - let now = SystemTime::now(); let event_tx_id = event_log_db.make_transaction_id(now, "sync")?; let execute_options = ExecuteRebasePlanOptions { @@ -180,7 +120,239 @@ pub fn sync( render_smartlog: false, }, }; + let thread_pool = ThreadPoolBuilder::new().build()?; + let repo_pool = RepoResource::new_pool(&repo)?; + + if pull { + let exit_code = execute_main_branch_sync_plan( + effects, + git_run_info, + &repo, + &mut dag, + &event_log_db, + &build_options, + &execute_options, + &thread_pool, + &repo_pool, + )?; + if !exit_code.is_success() { + return Ok(exit_code); + } + } + + // The main branch OID might have changed since we synced with `master`, so read it again. + let main_branch_oid = repo.get_main_branch_oid()?; + execute_sync_plans( + effects, + git_run_info, + &repo, + &event_log_db, + &mut dag, + &root_commit_oids, + root_commits, + main_branch_oid, + &build_options, + &execute_options, + &thread_pool, + &repo_pool, + ) +} + +fn execute_main_branch_sync_plan( + effects: &Effects, + git_run_info: &GitRunInfo, + repo: &Repo, + dag: &mut Dag, + event_log_db: &EventLogDb, + build_options: &BuildRebasePlanOptions, + execute_options: &ExecuteRebasePlanOptions, + thread_pool: &ThreadPool, + repo_pool: &RepoPool, +) -> eyre::Result { + let main_branch = repo.get_main_branch()?; + let upstream_main_branch = match main_branch.get_upstream_branch()? { + Some(upstream_main_branch) => upstream_main_branch, + None => return Ok(ExitCode(0)), + }; + let upstream_main_branch_oid = match upstream_main_branch.get_oid()? { + Some(upstream_main_branch_oid) => upstream_main_branch_oid, + None => return Ok(ExitCode(0)), + }; + dag.sync_from_oids( + effects, + repo, + CommitSet::from(upstream_main_branch_oid), + CommitSet::empty(), + )?; + let local_main_branch_commits = dag.query().only( + main_branch.get_oid()?.into_iter().collect(), + CommitSet::from(upstream_main_branch_oid), + )?; + + let main_branch_reference_name = main_branch.get_reference_name()?; + let branch_description = printable_styled_string( + effects.get_glyphs(), + StyledStringBuilder::new() + .append_styled( + CategorizedReferenceName::new(&main_branch_reference_name).friendly_describe(), + BaseColor::Green.dark(), + ) + .build(), + )?; + if local_main_branch_commits.is_empty()? { + writeln!( + effects.get_output_stream(), + "Fast-forwarding {}", + branch_description + )?; + repo.create_reference( + &main_branch_reference_name, + upstream_main_branch_oid, + true, + "sync", + )?; + return Ok(ExitCode(0)); + } else { + writeln!( + effects.get_output_stream(), + "Syncing {}", + branch_description + )?; + } + + let build_options = BuildRebasePlanOptions { + // Since we're syncing the main branch, by definition, any commits on it would be public, so + // we need to set this to `true` to get the rebase to succeed. + force_rewrite_public_commits: true, + ..build_options.clone() + }; + let permissions = match RebasePlanPermissions::verify_rewrite_set( + dag, + &build_options, + &local_main_branch_commits, + )? { + Ok(permissions) => permissions, + Err(err) => { + err.describe(effects, repo)?; + return Ok(ExitCode(1)); + } + }; + let mut builder = RebasePlanBuilder::new(dag, permissions); + let local_main_branch_roots = dag.query().roots(local_main_branch_commits)?; + let root_commit_oid = match commit_set_to_vec(&local_main_branch_roots)? + .into_iter() + .exactly_one() + { + Ok(root_oid) => root_oid, + Err(_) => return Ok(ExitCode(0)), + }; + builder.move_subtree(root_commit_oid, vec![upstream_main_branch_oid])?; + let rebase_plan = match builder.build(effects, thread_pool, repo_pool)? { + Ok(rebase_plan) => rebase_plan, + Err(err) => { + err.describe(effects, repo)?; + return Ok(ExitCode(1)); + } + }; + let rebase_plan = match rebase_plan { + Some(rebase_plan) => rebase_plan, + None => return Ok(ExitCode(0)), + }; + + execute_plans( + effects, + git_run_info, + repo, + event_log_db, + execute_options, + vec![(root_commit_oid, Some(rebase_plan))], + ) +} + +fn execute_sync_plans( + effects: &Effects, + git_run_info: &GitRunInfo, + repo: &Repo, + event_log_db: &EventLogDb, + dag: &mut Dag, + root_commit_oids: &CommitSet, + root_commits: Vec, + main_branch_oid: NonZeroOid, + build_options: &BuildRebasePlanOptions, + execute_options: &ExecuteRebasePlanOptions, + thread_pool: &ThreadPool, + repo_pool: &ResourcePool, +) -> eyre::Result { + let permissions = + match RebasePlanPermissions::verify_rewrite_set(dag, build_options, root_commit_oids)? { + Ok(permissions) => permissions, + Err(err) => { + err.describe(effects, repo)?; + return Ok(ExitCode(1)); + } + }; + let builder = RebasePlanBuilder::new(dag, permissions); + + let root_commit_oids = root_commits + .into_iter() + .map(|commit| commit.get_oid()) + .collect_vec(); + let root_commit_and_plans = thread_pool.install(|| -> eyre::Result<_> { + let result = root_commit_oids + // Don't parallelize for now, since the status updates don't render well. + .into_iter() + .map( + |root_commit_oid| -> eyre::Result< + Result<(NonZeroOid, Option), BuildRebasePlanError>, + > { + // Keep access to the same underlying caches by cloning the same instance of the builder. + let mut builder = builder.clone(); + + let repo = repo_pool.try_create()?; + let root_commit = repo.find_commit_or_fail(root_commit_oid)?; + + let only_parent_id = + root_commit.get_only_parent().map(|parent| parent.get_oid()); + if only_parent_id == Some(main_branch_oid) { + return Ok(Ok((root_commit_oid, None))); + } + + builder.move_subtree(root_commit.get_oid(), vec![main_branch_oid])?; + let rebase_plan = builder.build(effects, thread_pool, repo_pool)?; + Ok(rebase_plan.map(|rebase_plan| (root_commit_oid, rebase_plan))) + }, + ) + .collect::>>()? + .into_iter() + .collect::, BuildRebasePlanError>>(); + Ok(result) + })?; + + let root_commit_and_plans = match root_commit_and_plans { + Ok(root_commit_and_plans) => root_commit_and_plans, + Err(err) => { + err.describe(effects, repo)?; + return Ok(ExitCode(1)); + } + }; + execute_plans( + effects, + git_run_info, + repo, + event_log_db, + execute_options, + root_commit_and_plans, + ) +} +fn execute_plans( + effects: &Effects, + git_run_info: &GitRunInfo, + repo: &Repo, + event_log_db: &EventLogDb, + execute_options: &ExecuteRebasePlanOptions, + root_commit_and_plans: Vec<(NonZeroOid, Option)>, +) -> Result { let (success_commits, merge_conflict_commits, skipped_commits) = { let mut success_commits: Vec = Vec::new(); let mut merge_conflict_commits: Vec = Vec::new(); @@ -202,10 +374,10 @@ pub fn sync( let result = execute_rebase_plan( &effects, git_run_info, - &repo, - &event_log_db, + repo, + event_log_db, &rebase_plan, - &execute_options, + execute_options, )?; progress.notify_progress_inc(1); match result { @@ -229,10 +401,10 @@ pub fn sync( effects.get_output_stream(), "{}", printable_styled_string( - &glyphs, + effects.get_glyphs(), StyledStringBuilder::new() .append_plain("Synced ") - .append(success_commit.friendly_describe(&glyphs)?) + .append(success_commit.friendly_describe(effects.get_glyphs())?) .build() )? )?; @@ -243,10 +415,10 @@ pub fn sync( effects.get_output_stream(), "{}", printable_styled_string( - &glyphs, + effects.get_glyphs(), StyledStringBuilder::new() .append_plain("Merge conflict for ") - .append(merge_conflict_commit.friendly_describe(&glyphs)?) + .append(merge_conflict_commit.friendly_describe(effects.get_glyphs())?) .build() )? )?; @@ -256,7 +428,10 @@ pub fn sync( writeln!( effects.get_output_stream(), "Not moving up-to-date stack at {}", - printable_styled_string(&glyphs, skipped_commit.friendly_describe(&glyphs)?)? + printable_styled_string( + effects.get_glyphs(), + skipped_commit.friendly_describe(effects.get_glyphs())? + )? )?; } diff --git a/git-branchless/src/opts.rs b/git-branchless/src/opts.rs index 34e95c8c4..3690ff652 100644 --- a/git-branchless/src/opts.rs +++ b/git-branchless/src/opts.rs @@ -485,7 +485,7 @@ pub enum Command { visible_short_alias = 'u', visible_alias = "--update" )] - update_refs: bool, + pull: bool, /// Options for moving commits. #[clap(flatten)] diff --git a/git-branchless/tests/command/test_move.rs b/git-branchless/tests/command/test_move.rs index 97c8825fa..2d3ed51ba 100644 --- a/git-branchless/tests/command/test_move.rs +++ b/git-branchless/tests/command/test_move.rs @@ -4069,12 +4069,14 @@ fn test_move_dest_not_in_dag() -> eyre::Result<()> { insta::assert_snapshot!(stdout, @r###" Attempting rebase in-memory... [1/1] Committed as: 70deb1e create test3.txt - branchless: processing 2 updates: branch other-branch, remote branch origin/other-branch + branchless: processing 1 update: branch other-branch branchless: processing 1 rewritten commit branchless: running command: checkout other-branch - Your branch is up to date with 'origin/other-branch'. + Your branch and 'origin/other-branch' have diverged, + and have 2 and 1 different commits each, respectively. + (use "git pull" to merge the remote branch into yours) : - @ 70deb1e (> other-branch, remote origin/other-branch) create test3.txt + @ 70deb1e (> other-branch) create test3.txt In-memory rebase succeeded. "###); } diff --git a/git-branchless/tests/command/test_navigation.rs b/git-branchless/tests/command/test_navigation.rs index f2c08e393..b07ef690a 100644 --- a/git-branchless/tests/command/test_navigation.rs +++ b/git-branchless/tests/command/test_navigation.rs @@ -874,7 +874,7 @@ fn test_switch_auto_switch_interactive() -> eyre::Result<()> { "###); } - return Ok(()); + Ok(()) } #[test] @@ -917,5 +917,5 @@ fn test_switch_auto_switch_interactive_disabled() -> eyre::Result<()> { "###); } - return Ok(()); + Ok(()) } diff --git a/git-branchless/tests/command/test_smartlog.rs b/git-branchless/tests/command/test_smartlog.rs index 0b7ff0d72..ccc21c2fd 100644 --- a/git-branchless/tests/command/test_smartlog.rs +++ b/git-branchless/tests/command/test_smartlog.rs @@ -303,57 +303,6 @@ fn test_custom_main_branch() -> eyre::Result<()> { Ok(()) } -#[test] -fn test_main_remote_branch() -> eyre::Result<()> { - let GitWrapperWithRemoteRepo { - temp_dir: _guard, - original_repo, - cloned_repo, - } = make_git_with_remote_repo()?; - - { - original_repo.init_repo()?; - original_repo.commit_file("test1", 1)?; - original_repo.run(&[ - "clone", - original_repo.repo_path.to_str().unwrap(), - cloned_repo.repo_path.to_str().unwrap(), - ])?; - } - - { - cloned_repo.init_repo_with_options(&GitInitOptions { - make_initial_commit: false, - ..Default::default() - })?; - cloned_repo.detach_head()?; - cloned_repo.run(&["config", "branchless.core.mainBranch", "origin/master"])?; - cloned_repo.run(&["branch", "-d", "master"])?; - let (stdout, _stderr) = cloned_repo.run(&["smartlog"])?; - insta::assert_snapshot!(stdout, @r###" - : - @ 62fc20d (remote origin/master) create test1.txt - "###); - } - - { - original_repo.commit_file("test2", 2)?; - } - - { - cloned_repo.run(&["fetch"])?; - let (stdout, _stderr) = cloned_repo.run(&["smartlog"])?; - insta::assert_snapshot!(stdout, @r###" - : - @ 62fc20d create test1.txt - | - O 96d1c37 (remote origin/master) create test2.txt - "###); - } - - Ok(()) -} - #[test] fn test_show_rewritten_commit_hash() -> eyre::Result<()> { let git = make_git()?; @@ -539,19 +488,7 @@ fn test_active_non_head_main_branch_commit() -> eyre::Result<()> { let (stdout, _stderr) = cloned_repo.run(&["smartlog"])?; insta::assert_snapshot!(stdout, @r###" : - @ 70deb1e (> master, remote origin/master) create test3.txt - "###); - } - - { - // Verify that both `origin/master` and `master` appear in the smartlog. - cloned_repo.commit_file("test4", 4)?; - let (stdout, _stderr) = cloned_repo.run(&["smartlog"])?; - insta::assert_snapshot!(stdout, @r###" - : - O 70deb1e (remote origin/master) create test3.txt - | - @ 355e173 (> master) create test4.txt + @ 70deb1e (> master) create test3.txt "###); } diff --git a/git-branchless/tests/command/test_sync.rs b/git-branchless/tests/command/test_sync.rs index b0b3a28d5..91e160ac6 100644 --- a/git-branchless/tests/command/test_sync.rs +++ b/git-branchless/tests/command/test_sync.rs @@ -1,9 +1,25 @@ use lib::testing::{make_git, make_git_with_remote_repo, GitInitOptions, GitWrapperWithRemoteRepo}; +fn remove_nondeterministic_lines(output: String) -> String { + output + .lines() + .filter(|line| { + // This line is not present in some Git versions. + !line.contains("Fetching") + // This line is produced in a different order in some Git versions. + && !line.contains("Your branch is up to date") + }) + .map(|line| format!("{line}\n")) + .collect() +} + #[test] fn test_sync_basic() -> eyre::Result<()> { let git = make_git()?; + if !git.supports_reference_transactions()? { + return Ok(()); + } git.init_repo()?; git.detach_head()?; @@ -83,6 +99,10 @@ fn test_sync_basic() -> eyre::Result<()> { #[test] fn test_sync_up_to_date() -> eyre::Result<()> { let git = make_git()?; + + if !git.supports_reference_transactions()? { + return Ok(()); + } git.init_repo()?; git.commit_file("test1", 1)?; @@ -108,6 +128,9 @@ fn test_sync_pull() -> eyre::Result<()> { original_repo, cloned_repo, } = make_git_with_remote_repo()?; + if !original_repo.supports_reference_transactions()? { + return Ok(()); + } original_repo.init_repo()?; original_repo.commit_file("test1", 1)?; @@ -130,7 +153,7 @@ fn test_sync_pull() -> eyre::Result<()> { let (stdout, _stderr) = cloned_repo.run(&["smartlog"])?; insta::assert_snapshot!(stdout, @r###" : - O 96d1c37 (master, remote origin/master) create test2.txt + O 96d1c37 (master) create test2.txt | @ 70deb1e (> foo) create test3.txt "###); @@ -148,6 +171,7 @@ fn test_sync_pull() -> eyre::Result<()> { .collect(); insta::assert_snapshot!(stdout, @r###" branchless: running command: fetch --all + Fast-forwarding branch master Attempting rebase in-memory... [1/1] Committed as: 8e521a1 create test3.txt branchless: processing 1 update: branch foo @@ -165,9 +189,7 @@ fn test_sync_pull() -> eyre::Result<()> { let (stdout, _stderr) = cloned_repo.run(&["smartlog"])?; insta::assert_snapshot!(stdout, @r###" : - O 96d1c37 (master) create test2.txt - | - O d2e18e3 (remote origin/master) create test5.txt + O d2e18e3 (master) create test5.txt | @ 8e521a1 (> foo) create test3.txt "###); @@ -179,6 +201,10 @@ fn test_sync_pull() -> eyre::Result<()> { #[test] fn test_sync_specific_commit() -> eyre::Result<()> { let git = make_git()?; + + if !git.supports_reference_transactions()? { + return Ok(()); + } git.init_repo()?; git.commit_file("test1", 1)?; @@ -233,3 +259,136 @@ fn test_sync_specific_commit() -> eyre::Result<()> { Ok(()) } + +#[test] +fn test_sync_divergent_main_branch() -> eyre::Result<()> { + let GitWrapperWithRemoteRepo { + temp_dir: _guard, + original_repo, + cloned_repo, + } = make_git_with_remote_repo()?; + if !original_repo.supports_reference_transactions()? { + return Ok(()); + } + + original_repo.init_repo()?; + original_repo.commit_file("test1", 1)?; + original_repo.commit_file("test2", 2)?; + + original_repo.clone_repo_into(&cloned_repo, &["--branch", "master"])?; + cloned_repo.init_repo_with_options(&GitInitOptions { + make_initial_commit: false, + ..Default::default() + })?; + + original_repo.commit_file("test3", 3)?; + original_repo.commit_file("test4", 4)?; + cloned_repo.commit_file("test5", 5)?; + + { + let (stdout, _stderr) = cloned_repo.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + @ d2e18e3 (> master) create test5.txt + "###); + } + + { + let (stdout, _stderr) = cloned_repo.run(&["sync", "-p"])?; + let stdout = remove_nondeterministic_lines(stdout); + insta::assert_snapshot!(stdout, @r###" + branchless: running command: fetch --all + Syncing branch master + Attempting rebase in-memory... + [1/1] Committed as: f81d55c create test5.txt + branchless: processing 1 update: branch master + branchless: processing 1 rewritten commit + branchless: running command: checkout master + Your branch is ahead of 'origin/master' by 1 commit. + (use "git push" to publish your local commits) + In-memory rebase succeeded. + Synced d2e18e3 create test5.txt + "###); + } + + { + let (stdout, _stderr) = cloned_repo.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + @ f81d55c (> master) create test5.txt + "###); + } + + Ok(()) +} + +#[test] +fn test_sync_no_delete_main_branch() -> eyre::Result<()> { + let GitWrapperWithRemoteRepo { + temp_dir: _guard, + original_repo, + cloned_repo, + } = make_git_with_remote_repo()?; + if !original_repo.supports_reference_transactions()? { + return Ok(()); + } + + original_repo.init_repo()?; + original_repo.commit_file("test1", 1)?; + original_repo.commit_file("test2", 2)?; + original_repo.run(&["checkout", "-b", "foo"])?; + original_repo.commit_file("test3", 3)?; + + original_repo.clone_repo_into(&cloned_repo, &["--branch", "master"])?; + cloned_repo.init_repo_with_options(&GitInitOptions { + make_initial_commit: false, + ..Default::default() + })?; + cloned_repo.run(&["reset", "--hard", "HEAD^"])?; + + // Simulate landing the commit upstream with a potentially different commit + // hash. + cloned_repo.run(&["cherry-pick", "origin/master"])?; + cloned_repo.run(&["commit", "--amend", "-m", "updated commit message"])?; + + cloned_repo.run(&["branch", "should-be-deleted"])?; + + { + let (stdout, stderr) = cloned_repo.run(&["sync", "-p", "--on-disk"])?; + let stdout = remove_nondeterministic_lines(stdout); + let stderr = remove_nondeterministic_lines(stderr); + insta::assert_snapshot!(stderr, @r###" + branchless: processing 1 update: ref HEAD + Executing: git branchless hook-skip-upstream-applied-commit 6ffd720862b7ae71cbe30d66ed27ea8579e24b0f + Executing: git branchless hook-register-extra-post-rewrite-hook + branchless: processing 1 rewritten commit + branchless: processing 2 updates: branch master, branch should-be-deleted + branchless: creating working copy snapshot + branchless: running command: checkout master + Switched to branch 'master' + branchless: processing checkout + : + @ 96d1c37 (> master) create test2.txt + Successfully rebased and updated detached HEAD. + "###); + insta::assert_snapshot!(stdout, @r###" + branchless: running command: fetch --all + Syncing branch master + branchless: running command: diff --quiet + Calling Git for on-disk rebase... + branchless: running command: rebase --continue + Skipping commit (was already applied upstream): 6ffd720 updated commit message + Synced 6ffd720 updated commit message + "###); + } + + { + let (stdout, _stderr) = cloned_repo.run(&["smartlog"])?; + insta::assert_snapshot!(stdout, @r###" + : + @ 96d1c37 (> master) create test2.txt + "###); + } + + Ok(()) +} diff --git a/git-branchless/tests/command/test_undo.rs b/git-branchless/tests/command/test_undo.rs index 13e6f49f9..21dee482a 100644 --- a/git-branchless/tests/command/test_undo.rs +++ b/git-branchless/tests/command/test_undo.rs @@ -921,55 +921,58 @@ fn test_undo_no_confirm() -> eyre::Result<()> { #[test] fn test_undo_unseen_commit() -> eyre::Result<()> { - let git = make_git()?; - if !git.supports_reference_transactions()? { - return Ok(()); - } - - git.init_repo_with_options(&GitInitOptions { - run_branchless_init: false, - make_initial_commit: true, - })?; - git.commit_file("test1", 1)?; - git.detach_head()?; - let test2_oid = git.commit_file("test2", 2)?; - git.run(&["checkout", "HEAD^"])?; - let test3_oid = git.commit_file("test3", 3)?; - git.run(&["checkout", "master"])?; - git.run(&["branchless", "init"])?; - - // Move the remote-tracking branch to a commit (test3) which doesn't have the previous location - // (test2) as an ancestor, to ensure that the DAG won't have observed it. - git.run(&[ - "update-ref", - "refs/remotes/origin/master", - &test2_oid.to_string(), - ])?; - git.run(&[ - "update-ref", - "refs/remotes/origin/master", - &test3_oid.to_string(), - &test2_oid.to_string(), - ])?; - - // Set the main branch to `origin/master` to ensure that it appears in historical smartlogs. - git.run(&["remote", "add", "origin", "file:///some-remote"])?; - git.run(&["branch", "-u", "origin/master"])?; - git.run(&["branchless", "init", "--main-branch", "origin/master"])?; - - { - let screenshot1 = Default::default(); - run_select_past_event( - &git.get_repo()?, - vec![ - CursiveTestingEvent::Event('p'.into()), - CursiveTestingEvent::Event('p'.into()), - CursiveTestingEvent::Event('p'.into()), - CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)), - CursiveTestingEvent::Event('q'.into()), - ], - )?; - insta::assert_snapshot!(screen_to_string(&screenshot1), @r###" + // Disabled since we no longer support `origin/master` as a main branch, but this test might be + // useful in the future. + if cfg!(none) { + let git = make_git()?; + if !git.supports_reference_transactions()? { + return Ok(()); + } + + git.init_repo_with_options(&GitInitOptions { + run_branchless_init: false, + make_initial_commit: true, + })?; + git.commit_file("test1", 1)?; + git.detach_head()?; + let test2_oid = git.commit_file("test2", 2)?; + git.run(&["checkout", "HEAD^"])?; + let test3_oid = git.commit_file("test3", 3)?; + git.run(&["checkout", "master"])?; + git.run(&["branchless", "init"])?; + + // Move the remote-tracking branch to a commit (test3) which doesn't have the previous location + // (test2) as an ancestor, to ensure that the DAG won't have observed it. + git.run(&[ + "update-ref", + "refs/remotes/origin/master", + &test2_oid.to_string(), + ])?; + git.run(&[ + "update-ref", + "refs/remotes/origin/master", + &test3_oid.to_string(), + &test2_oid.to_string(), + ])?; + + // Set the main branch to `origin/master` to ensure that it appears in historical smartlogs. + git.run(&["remote", "add", "origin", "file:///some-remote"])?; + git.run(&["branch", "-u", "origin/master"])?; + git.run(&["branchless", "init", "--main-branch", "origin/master"])?; + + { + let screenshot1 = Default::default(); + run_select_past_event( + &git.get_repo()?, + vec![ + CursiveTestingEvent::Event('p'.into()), + CursiveTestingEvent::Event('p'.into()), + CursiveTestingEvent::Event('p'.into()), + CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)), + CursiveTestingEvent::Event('q'.into()), + ], + )?; + insta::assert_snapshot!(screen_to_string(&screenshot1), @r###" ┌───────────────────────────────────────────────────┤ Commit graph ├───────────────────────────────────────────────────┐ │: │ │O 4838e49 (remote origin/master) create test3.txt │ @@ -995,6 +998,7 @@ fn test_undo_unseen_commit() -> eyre::Result<()> { │There are no previous available events. │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ "###); + } } Ok(())