diff --git a/src/errors.rs b/src/errors.rs index e1f78eb..d3a796a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -41,6 +41,11 @@ pub enum StError { /// A generic decoding error occurred. #[error("Decoding error: {}", .0)] DecodingError(String), + /// No stack found or stack is empty. + #[error( + "No stack found. The current branch might be the trunk branch or no branches are tracked." + )] + NoStackFound, // ---- [ `st` application errors (remote) ] ---- /// A remote pull request could not be found. diff --git a/src/subcommands/local/bottom.rs b/src/subcommands/local/bottom.rs new file mode 100644 index 0000000..dec1ca6 --- /dev/null +++ b/src/subcommands/local/bottom.rs @@ -0,0 +1,39 @@ +//! `bottom` subcommand. + +use crate::{ + ctx::StContext, + errors::{StError, StResult}, + git::RepositoryExt, +}; +use clap::Args; +use nu_ansi_term::Color; + +/// CLI arguments for the `bottom` subcommand. +#[derive(Debug, Clone, Eq, PartialEq, Args)] +pub struct BottomCmd; + +impl BottomCmd { + /// Run the `bottom` subcommand. + pub fn run(self, ctx: StContext<'_>) -> StResult<()> { + // Discover the current stack + let stack = ctx.discover_stack()?; + + // If the stack only has the trunk branch, there's no bottom branch to move to + if stack.len() <= 1 { + return Err(StError::NoStackFound); + } + + // The bottom branch is the first branch after the trunk + let bottom_branch = &stack[1]; + + // Check out the bottom branch + ctx.repository.checkout_branch(bottom_branch)?; + + println!( + "Moved to bottom branch of stack: `{}`", + Color::Green.paint(bottom_branch) + ); + + Ok(()) + } +} diff --git a/src/subcommands/local/down.rs b/src/subcommands/local/down.rs new file mode 100644 index 0000000..075f553 --- /dev/null +++ b/src/subcommands/local/down.rs @@ -0,0 +1,53 @@ +//! `down` subcommand. + +use crate::{ + ctx::StContext, + errors::{StError, StResult}, + git::RepositoryExt, +}; +use clap::Args; +use nu_ansi_term::Color; + +/// CLI arguments for the `down` subcommand. +#[derive(Debug, Clone, Eq, PartialEq, Args)] +pub struct DownCmd; + +impl DownCmd { + /// Run the `down` subcommand. + pub fn run(self, ctx: StContext<'_>) -> StResult<()> { + // Discover the current stack + let stack = ctx.discover_stack()?; + + // If the stack only has the trunk branch, there's no place to move up to + if stack.len() <= 1 { + return Err(StError::NoStackFound); + } + + // Get the current branch name + let current_branch = ctx.repository.current_branch_name()?; + + // Find the current branch in the stack + let current_index = stack + .iter() + .position(|branch| branch == ¤t_branch) + .ok_or(StError::BranchNotTracked(current_branch.clone()))?; + + // If we're already at the trunk branch (index 0), can't go up any further + if current_index == 0 { + println!( + "Already at trunk branch `{}`. Cannot move down further.", + Color::Green.paint(¤t_branch) + ); + return Ok(()); + } + + let down_branch = &stack[current_index - 1]; + + // Check out the branch below + ctx.repository.checkout_branch(down_branch)?; + + println!("Moved down to: `{}`", Color::Green.paint(down_branch)); + + Ok(()) + } +} diff --git a/src/subcommands/local/mod.rs b/src/subcommands/local/mod.rs index b83cedc..3d613b3 100644 --- a/src/subcommands/local/mod.rs +++ b/src/subcommands/local/mod.rs @@ -23,3 +23,15 @@ pub use untrack::UntrackCmd; mod config; pub use config::ConfigCmd; + +mod bottom; +pub use bottom::BottomCmd; + +mod top; +pub use top::TopCmd; + +mod up; +pub use up::UpCmd; + +mod down; +pub use down::DownCmd; diff --git a/src/subcommands/local/top.rs b/src/subcommands/local/top.rs new file mode 100644 index 0000000..d4f5fdf --- /dev/null +++ b/src/subcommands/local/top.rs @@ -0,0 +1,39 @@ +//! `top` subcommand. + +use crate::{ + ctx::StContext, + errors::{StError, StResult}, + git::RepositoryExt, +}; +use clap::Args; +use nu_ansi_term::Color; + +/// CLI arguments for the `top` subcommand. +#[derive(Debug, Clone, Eq, PartialEq, Args)] +pub struct TopCmd; + +impl TopCmd { + /// Run the `top` subcommand. + pub fn run(self, ctx: StContext<'_>) -> StResult<()> { + // Discover the current stack + let stack = ctx.discover_stack()?; + + // If the stack only has the trunk branch, there's no top branch to move to + if stack.len() <= 1 { + return Err(StError::NoStackFound); + } + + // The top branch is the last branch in the stack + let top_branch = &stack[stack.len() - 1]; + + // Check out the top branch + ctx.repository.checkout_branch(top_branch)?; + + println!( + "Moved to top branch of stack: `{}`", + Color::Green.paint(top_branch) + ); + + Ok(()) + } +} diff --git a/src/subcommands/local/up.rs b/src/subcommands/local/up.rs new file mode 100644 index 0000000..ac7132b --- /dev/null +++ b/src/subcommands/local/up.rs @@ -0,0 +1,53 @@ +//! `up` subcommand. + +use crate::{ + ctx::StContext, + errors::{StError, StResult}, + git::RepositoryExt, +}; +use clap::Args; +use nu_ansi_term::Color; + +/// CLI arguments for the `up` subcommand. +#[derive(Debug, Clone, Eq, PartialEq, Args)] +pub struct UpCmd; + +impl UpCmd { + /// Run the `up` subcommand. + pub fn run(self, ctx: StContext<'_>) -> StResult<()> { + // Discover the current stack + let stack = ctx.discover_stack()?; + + // If the stack only has the trunk branch, there's no place to move up to + if stack.len() <= 1 { + return Err(StError::NoStackFound); + } + + // Get the current branch name + let current_branch = ctx.repository.current_branch_name()?; + + // Find the current branch in the stack + let current_index = stack + .iter() + .position(|branch| branch == ¤t_branch) + .ok_or(StError::BranchNotTracked(current_branch.clone()))?; + + // If we're already at the trunk branch (index 0), can't go up any further + if current_index == stack.len() - 1 { + println!( + "Already at topmost branch `{}`. Cannot move up further.", + Color::Green.paint(¤t_branch) + ); + return Ok(()); + } + + let up_branch = &stack[current_index + 1]; + + // Check out the branch above + ctx.repository.checkout_branch(up_branch)?; + + println!("Moved up to: `{}`", Color::Green.paint(up_branch)); + + Ok(()) + } +} diff --git a/src/subcommands/mod.rs b/src/subcommands/mod.rs index 2a6e8dd..af55535 100644 --- a/src/subcommands/mod.rs +++ b/src/subcommands/mod.rs @@ -5,7 +5,8 @@ use clap::Subcommand; mod local; use local::{ - CheckoutCmd, ConfigCmd, CreateCmd, DeleteCmd, LogCmd, RestackCmd, TrackCmd, UntrackCmd, + BottomCmd, CheckoutCmd, ConfigCmd, CreateCmd, DeleteCmd, DownCmd, LogCmd, RestackCmd, TopCmd, + TrackCmd, UntrackCmd, UpCmd, }; mod remote; @@ -46,6 +47,18 @@ pub enum Subcommands { /// Configure the st application. #[clap(visible_alias = "cfg")] Config(ConfigCmd), + /// Move to the bottom branch of the stack (first branch after trunk). + #[clap(visible_alias = "b")] + Bottom(BottomCmd), + /// Move to the top branch of the stack (tip of the stack). + #[clap(visible_alias = "t")] + Top(TopCmd), + /// Move to the branch above the current branch. + #[clap(visible_alias = "u")] + Up(UpCmd), + /// Move to the branch below the current branch. + #[clap(visible_alias = "d")] + Down(DownCmd), } impl Subcommands { @@ -65,6 +78,10 @@ impl Subcommands { Self::Track(args) => args.run(ctx), Self::Untrack(args) => args.run(ctx), Self::Config(args) => args.run(ctx), + Self::Bottom(args) => args.run(ctx), + Self::Top(args) => args.run(ctx), + Self::Up(args) => args.run(ctx), + Self::Down(args) => args.run(ctx), } } }