diff --git a/crates/but/src/command/legacy/base.rs b/crates/but/src/command/legacy/base.rs deleted file mode 100644 index fe6f5b4b35..0000000000 --- a/crates/but/src/command/legacy/base.rs +++ /dev/null @@ -1,185 +0,0 @@ -use base::Subcommands; -use but_ctx::LegacyProject; -use colored::Colorize; -use gitbutler_branch_actions::upstream_integration::{ - BranchStatus::{Conflicted, Empty, Integrated, SaflyUpdatable}, - Resolution, ResolutionApproach, - StackStatuses::{UpToDate, UpdatesRequired}, -}; - -use crate::{args::base, utils::OutputChannel}; - -pub async fn handle( - cmd: Subcommands, - project: &LegacyProject, - out: &mut OutputChannel, -) -> anyhow::Result<()> { - match cmd { - Subcommands::Check => { - if let Some(out) = out.for_human() { - writeln!(out, "šŸ” Checking base branch status...")?; - let base_branch = but_api::legacy::virtual_branches::fetch_from_remotes( - project.id, - Some("auto".to_string()), - )?; - writeln!(out, "\nšŸ“ Base branch:\t\t{}", base_branch.branch_name)?; - writeln!( - out, - "ā« Upstream commits:\t{} new commits on {}\n", - base_branch.behind, base_branch.branch_name - )?; - let commits = base_branch.recent_commits.iter().take(3); - for commit in commits { - writeln!( - out, - "\t{} {}", - &commit.id[..7], - &commit - .description - .to_string() - .replace('\n', " ") - .chars() - .take(72) - .collect::() - )?; - } - let hidden_commits = base_branch.behind.saturating_sub(3); - if hidden_commits > 0 { - writeln!( - out, - "\t... ({hidden_commits} more - run `but base check --all` to see all)" - )?; - } - - let status = but_api::legacy::virtual_branches::upstream_integration_statuses( - project.id, None, - ) - .await?; - - match status { - UpToDate => { - writeln!(out, "\nāœ… Everything is up to date")?; - } - UpdatesRequired { - worktree_conflicts, - statuses, - } => { - if !worktree_conflicts.is_empty() { - writeln!( - out, - "\nā—ļø There are uncommitted changes in the worktree that may conflict with the updates." - )?; - } - if !statuses.is_empty() { - writeln!(out, "\n{}", "Active Branch Status".bold())?; - for (_id, status) in statuses { - for bs in status.branch_statuses { - let status_icon = match bs.status { - SaflyUpdatable => "āœ…".to_string(), - Integrated => "šŸ”„".to_string(), - Conflicted { rebasable } => { - if rebasable { - "āš ļø".to_string() - } else { - "ā—ļø".to_string() - } - } - Empty => "āœ…".to_string(), - }; - let status_text = match bs.status { - SaflyUpdatable => "Updatable".green(), - Integrated => "Integrated".blue(), - Conflicted { rebasable } => { - if rebasable { - "Conflicted (Rebasable)".yellow() - } else { - "Conflicted (Not Rebasable)".red() - } - } - Empty => "Nothing to do".normal(), - }; - writeln!( - out, - "\n{} {} ({})", - status_icon, bs.name, status_text - )?; - } - } - } - } - } - writeln!(out, "\nRun `but base update` to update your branches")?; - } - Ok(()) - } - Subcommands::Update => { - let status = - but_api::legacy::virtual_branches::upstream_integration_statuses(project.id, None) - .await?; - let resolutions = match status { - UpToDate => { - if let Some(out) = out.for_human() { - writeln!(out, "āœ… Everything is up to date")?; - } - None - } - UpdatesRequired { - worktree_conflicts, - statuses, - } => { - if !worktree_conflicts.is_empty() { - if let Some(out) = out.for_human() { - writeln!( - out, - "ā—ļø There are uncommitted changes in the worktree that may conflict with - the updates. Please commit or stash them and try again." - )?; - } - None - } else { - if let Some(out) = out.for_human() { - writeln!(out, "šŸ”„ Updating branches...")?; - } - let mut resolutions = vec![]; - for (maybe_stack_id, status) in statuses { - let Some(stack_id) = maybe_stack_id else { - if let Some(out) = out.for_human() { - writeln!( - out, - "No stack ID, assuming we're on single-branch mode...", - )?; - } - continue; - }; - let approach = if status.branch_statuses.iter().all(|s| s.status == Integrated) - && status.tree_status - != gitbutler_branch_actions::upstream_integration::TreeStatus::Conflicted - { - ResolutionApproach::Delete - } else { - ResolutionApproach::Rebase - }; - let resolution = Resolution { - stack_id, - approach, - delete_integrated_branches: true, - }; - resolutions.push(resolution); - } - Some(resolutions) - } - } - }; - - if let Some(resolutions) = resolutions { - but_api::legacy::virtual_branches::integrate_upstream( - project.id, - resolutions, - None, - ) - .await?; - } - Ok(()) - } - } -} diff --git a/crates/but/src/command/legacy/base/json.rs b/crates/but/src/command/legacy/base/json.rs new file mode 100644 index 0000000000..6bcb69ccb0 --- /dev/null +++ b/crates/but/src/command/legacy/base/json.rs @@ -0,0 +1,46 @@ +//! JSON output structures for `but base` commands. + +use serde::Serialize; + +/// JSON output for `but base check` +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct BaseCheckOutput { + pub base_branch: BaseBranchInfo, + pub upstream_commits: UpstreamInfo, + pub branch_statuses: Vec, + pub up_to_date: bool, + pub has_worktree_conflicts: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct BaseBranchInfo { + pub name: String, + pub remote_name: String, + pub base_sha: String, + pub current_sha: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct UpstreamInfo { + pub count: usize, + pub commits: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct UpstreamCommit { + pub id: String, + pub description: String, + pub author_name: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct BranchStatusInfo { + pub name: String, + pub status: String, + pub rebasable: Option, +} diff --git a/crates/but/src/command/legacy/base/mod.rs b/crates/but/src/command/legacy/base/mod.rs new file mode 100644 index 0000000000..c5214377e5 --- /dev/null +++ b/crates/but/src/command/legacy/base/mod.rs @@ -0,0 +1,248 @@ +mod json; + +use base::Subcommands; +use but_ctx::LegacyProject; +use colored::Colorize; +use gitbutler_branch_actions::upstream_integration::{ + BranchStatus::{Conflicted, Empty, Integrated, SaflyUpdatable}, + Resolution, ResolutionApproach, + StackStatuses::{UpToDate, UpdatesRequired}, +}; + +use crate::{args::base, utils::OutputChannel}; +use json::{BaseBranchInfo, BaseCheckOutput, BranchStatusInfo, UpstreamCommit, UpstreamInfo}; + +pub async fn handle( + cmd: Subcommands, + project: &LegacyProject, + out: &mut OutputChannel, +) -> anyhow::Result<()> { + match cmd { + Subcommands::Check => { + let base_branch = but_api::legacy::virtual_branches::fetch_from_remotes( + project.id, + Some("auto".to_string()), + )?; + + let status = + but_api::legacy::virtual_branches::upstream_integration_statuses(project.id, None) + .await?; + + if let Some(out) = out.for_json() { + let (up_to_date, has_worktree_conflicts, branch_statuses) = match &status { + UpToDate => (true, false, vec![]), + UpdatesRequired { + worktree_conflicts, + statuses, + } => { + let branch_statuses: Vec = statuses + .iter() + .flat_map(|(_id, stack_status)| { + stack_status.branch_statuses.iter().map(|bs| { + let (status_str, rebasable) = match bs.status { + SaflyUpdatable => ("updatable", None), + Integrated => ("integrated", None), + Conflicted { rebasable } => ("conflicted", Some(rebasable)), + Empty => ("empty", None), + }; + BranchStatusInfo { + name: bs.name.clone(), + status: status_str.to_string(), + rebasable, + } + }) + }) + .collect(); + (false, !worktree_conflicts.is_empty(), branch_statuses) + } + }; + + let output = BaseCheckOutput { + base_branch: BaseBranchInfo { + name: base_branch.branch_name.clone(), + remote_name: base_branch.remote_name.clone(), + base_sha: base_branch.base_sha.to_string(), + current_sha: base_branch.current_sha.to_string(), + }, + upstream_commits: UpstreamInfo { + count: base_branch.behind, + commits: base_branch + .upstream_commits + .iter() + .map(|c| UpstreamCommit { + id: c.id.clone(), + description: c.description.to_string(), + author_name: c.author.name.clone(), + }) + .collect(), + }, + branch_statuses, + up_to_date, + has_worktree_conflicts, + }; + out.write_value(output)?; + } else if let Some(out) = out.for_human() { + writeln!(out, "{}", "Checking base branch status...".bold())?; + writeln!( + out, + "\n{}\t{}", + "Base branch:".dimmed(), + base_branch.branch_name.cyan() + )?; + let upstream_label = format!( + "{} new commits on {}", + base_branch.behind, base_branch.branch_name + ); + writeln!( + out, + "{}\t{}", + "Upstream:".dimmed(), + if base_branch.behind > 0 { + upstream_label.yellow() + } else { + upstream_label.green() + } + )?; + + if !base_branch.upstream_commits.is_empty() { + writeln!(out)?; + let commits = base_branch.upstream_commits.iter().take(3); + for commit in commits { + writeln!( + out, + " {} {}", + commit.id[..7].yellow(), + commit + .description + .to_string() + .replace('\n', " ") + .chars() + .take(72) + .collect::() + .dimmed() + )?; + } + let hidden_commits = base_branch.behind.saturating_sub(3); + if hidden_commits > 0 { + writeln!(out, " {}", format!("... ({hidden_commits} more)").dimmed())?; + } + } + + match status { + UpToDate => { + writeln!(out, "\n{}", "Up to date".green().bold())?; + } + UpdatesRequired { + worktree_conflicts, + statuses, + } => { + if !worktree_conflicts.is_empty() { + writeln!( + out, + "\n{}", + "Warning: uncommitted changes may conflict with updates." + .yellow() + .bold() + )?; + } + if !statuses.is_empty() { + writeln!(out, "\n{}", "Branch Status".bold())?; + for (_id, status) in statuses { + for bs in status.branch_statuses { + let status_text = match bs.status { + SaflyUpdatable => "[ok]".green(), + Integrated => "[integrated]".blue(), + Conflicted { rebasable } => { + if rebasable { + "[conflict - rebasable]".yellow() + } else { + "[conflict]".red() + } + } + Empty => "[empty]".dimmed(), + }; + writeln!(out, " {} {}", status_text, bs.name)?; + } + } + } + writeln!( + out, + "\n{}", + "Run `but base update` to update your branches".dimmed() + )?; + } + } + } + Ok(()) + } + Subcommands::Update => { + let status = + but_api::legacy::virtual_branches::upstream_integration_statuses(project.id, None) + .await?; + let resolutions = match status { + UpToDate => { + if let Some(out) = out.for_human() { + writeln!(out, "āœ… Everything is up to date")?; + } + None + } + UpdatesRequired { + worktree_conflicts, + statuses, + } => { + if !worktree_conflicts.is_empty() { + if let Some(out) = out.for_human() { + writeln!( + out, + "ā—ļø There are uncommitted changes in the worktree that may conflict with + the updates. Please commit or stash them and try again." + )?; + } + None + } else { + if let Some(out) = out.for_human() { + writeln!(out, "šŸ”„ Updating branches...")?; + } + let mut resolutions = vec![]; + for (maybe_stack_id, status) in statuses { + let Some(stack_id) = maybe_stack_id else { + if let Some(out) = out.for_human() { + writeln!( + out, + "No stack ID, assuming we're on single-branch mode...", + )?; + } + continue; + }; + let approach = if status.branch_statuses.iter().all(|s| s.status == Integrated) + && status.tree_status + != gitbutler_branch_actions::upstream_integration::TreeStatus::Conflicted + { + ResolutionApproach::Delete + } else { + ResolutionApproach::Rebase + }; + let resolution = Resolution { + stack_id, + approach, + delete_integrated_branches: true, + }; + resolutions.push(resolution); + } + Some(resolutions) + } + } + }; + + if let Some(resolutions) = resolutions { + but_api::legacy::virtual_branches::integrate_upstream( + project.id, + resolutions, + None, + ) + .await?; + } + Ok(()) + } + } +}