Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
396 changes: 395 additions & 1 deletion Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ clap = { version = "4.5", features = ["derive"] }
color-eyre = "0.6"
owo-colors = { version = "4.0", features = ["supports-colors"] }
git2 = "0.20"
crossterm = "0.27"
ratatui = { version = "0.26", default-features = false, features = ["crossterm"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Expand Down
27 changes: 27 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
commands::{
cd::CdCommand,
create::CreateCommand,
interactive,
list::ListCommand,
merge_pr_github::MergePrGithubCommand,
pr_github::{PrGithubCommand, PrGithubOptions},
Expand All @@ -31,6 +32,9 @@ enum Commands {
Ls,
/// Open a shell in the given worktree.
Cd(CdArgs),
/// Interactively browse and open worktrees.
#[command(alias = "i")]
Interactive,
/// Remove a worktree tracked in `.rsworktree`.
Rm(RmArgs),
/// Create a GitHub pull request for the worktree's branch using the GitHub CLI.
Expand Down Expand Up @@ -97,6 +101,9 @@ struct PrGithubArgs {
struct MergePrGithubArgs {
/// Name of the worktree to merge the PR for (defaults to the current worktree)
name: Option<String>,
/// Remove the remote branch after merging
#[arg(long = "remove")]
remove_remote: bool,
}

pub fn run() -> color_eyre::Result<()> {
Expand All @@ -116,6 +123,9 @@ pub fn run() -> color_eyre::Result<()> {
let command = CdCommand::new(args.name, args.print);
command.execute(&repo)?;
}
Commands::Interactive => {
interactive::run(&repo)?;
}
Commands::Rm(args) => {
let command = RemoveCommand::new(args.name, args.force);
command.execute(&repo)?;
Expand All @@ -138,6 +148,9 @@ pub fn run() -> color_eyre::Result<()> {
Commands::MergePrGithub(args) => {
let worktree_name = resolve_worktree_name(args.name, &repo, "merge-pr-github")?;
let mut command = MergePrGithubCommand::new(worktree_name);
if args.remove_remote {
command.enable_remove_remote();
}
command.execute(&repo)?;
}
}
Expand Down Expand Up @@ -195,6 +208,7 @@ mod tests {
use super::*;
use std::{env, fs, path::Path, process::Command as StdCommand};

use clap::Parser;
use color_eyre::eyre::{self, WrapErr};

use tempfile::TempDir;
Expand Down Expand Up @@ -266,6 +280,19 @@ mod tests {
Ok(())
}

#[test]
fn parses_interactive_command_and_alias() -> color_eyre::Result<()> {
let interactive = Cli::try_parse_from(["rsworktree", "interactive"])
.expect("interactive subcommand should parse");
assert!(matches!(interactive.command, Commands::Interactive));

let alias =
Cli::try_parse_from(["rsworktree", "i"]).expect("interactive alias should parse");
assert!(matches!(alias.command, Commands::Interactive));

Ok(())
}

#[test]
fn resolve_worktree_name_infers_from_cwd_inside_worktree() -> color_eyre::Result<()> {
let repo_dir = TempDir::new()?;
Expand Down
103 changes: 64 additions & 39 deletions src/commands/create/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,59 @@ pub struct CreateCommand {
base: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CreateOutcome {
AlreadyExists,
Created,
}

impl CreateCommand {
pub fn new(name: String, base: Option<String>) -> Self {
Self { name, base }
}

pub fn execute(&self, repo: &Repo) -> color_eyre::Result<()> {
let outcome = self.create_internal(repo, false)?;
match outcome {
CreateOutcome::Created | CreateOutcome::AlreadyExists => self.enter_worktree(repo),
}
}

pub fn create_without_enter(
&self,
repo: &Repo,
quiet: bool,
) -> color_eyre::Result<CreateOutcome> {
self.create_internal(repo, quiet)
}

fn enter_worktree(&self, repo: &Repo) -> color_eyre::Result<()> {
CdCommand::new(self.name.clone(), false).execute(repo)
}

fn create_internal(&self, repo: &Repo, quiet: bool) -> color_eyre::Result<CreateOutcome> {
let worktrees_dir = repo.ensure_worktrees_dir()?;
let worktree_path = worktrees_dir.join(&self.name);
let target_branch = self.name.as_str();
let base_branch = self.base.as_deref();

if worktree_path.exists() {
let name = format!(
"{}",
self.name
.as_str()
.if_supports_color(Stream::Stdout, |text| {
format!("{}", text.cyan().bold())
})
);
println!(
"Worktree `{}` already exists at `{}`.",
name,
worktree_path.display()
);
return self.enter_worktree(repo);
if !quiet {
let name = format!(
"{}",
self.name
.as_str()
.if_supports_color(Stream::Stdout, |text| {
format!("{}", text.cyan().bold())
})
);
println!(
"Worktree `{}` already exists at `{}`.",
name,
worktree_path.display()
);
}
return Ok(CreateOutcome::AlreadyExists);
}

if let Some(parent) = worktree_path.parent() {
Expand All @@ -63,36 +90,34 @@ impl CreateCommand {
)
})?;

let name = format!(
"{}",
target_branch.if_supports_color(Stream::Stdout, |text| {
format!("{}", text.green().bold())
})
);
let path_raw = format!("{}", worktree_path.display());
let path = format!(
"{}",
path_raw
.as_str()
.if_supports_color(Stream::Stdout, |text| { format!("{}", text.blue()) })
);
if let Some(base) = base_branch {
let base = format!(
if !quiet {
let name = format!(
"{}",
base.if_supports_color(Stream::Stdout, |text| {
format!("{}", text.magenta().bold())
target_branch.if_supports_color(Stream::Stdout, |text| {
format!("{}", text.green().bold())
})
);
println!("Created worktree `{}` at `{}` from `{}`.", name, path, base);
} else {
println!("Created worktree `{}` at `{}`.", name, path);
let path_raw = format!("{}", worktree_path.display());
let path = format!(
"{}",
path_raw
.as_str()
.if_supports_color(Stream::Stdout, |text| { format!("{}", text.blue()) })
);
if let Some(base) = base_branch {
let base = format!(
"{}",
base.if_supports_color(Stream::Stdout, |text| {
format!("{}", text.magenta().bold())
})
);
println!("Created worktree `{}` at `{}` from `{}`.", name, path, base);
} else {
println!("Created worktree `{}` at `{}`.", name, path);
}
}

self.enter_worktree(repo)
}

fn enter_worktree(&self, repo: &Repo) -> color_eyre::Result<()> {
CdCommand::new(self.name.clone(), false).execute(repo)
Ok(CreateOutcome::Created)
}
}

Expand Down
Loading