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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
- Remove the named worktree. Pass `--force` to mirror `git worktree remove --force` behavior.
- Demo: ![Remove demo](tapes/gifs/rm.gif)

- `rsworktree pr-github [<name>] [--no-push] [--draft] [--fill] [--web] [--reviewer <login> ...] [-- <extra gh args>]`
- Push the worktree branch (unless `--no-push`) and invoke `gh pr create` with the provided options. When `<name>` is omitted, the command uses the current `.rsworktree/<name>` directory. If you don’t supply PR metadata flags, `rsworktree` automatically adds `--fill`; you can pass `--title/--body` or `--web` to override that behaviour.
- Demo: ![PR demo](tapes/gifs/pr_github.gif)
- Requires the [GitHub CLI](https://cli.github.com/) (`gh`) to be installed and on your `PATH`.

## Installation

Install from crates.io with:
Expand Down
91 changes: 90 additions & 1 deletion src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
use std::env;

use clap::{Parser, Subcommand};

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

use crate::{
Repo,
commands::{cd::CdCommand, create::CreateCommand, list::ListCommand, rm::RemoveCommand},
commands::{
cd::CdCommand, create::CreateCommand, list::ListCommand, pr_github::PrGithubCommand,
rm::RemoveCommand,
},
};

#[derive(Parser, Debug)]
Expand All @@ -22,6 +29,8 @@ enum Commands {
Cd(CdArgs),
/// Remove a worktree tracked in `.rsworktree`.
Rm(RmArgs),
/// Create a GitHub pull request for the worktree's branch using the GitHub CLI.
PrGithub(PrGithubArgs),
}

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -51,6 +60,33 @@ struct RmArgs {
force: bool,
}

#[derive(Parser, Debug)]
struct PrGithubArgs {
/// Name of the worktree to prepare a PR from (defaults to the current worktree)
name: Option<String>,
/// Skip pushing the branch before creating the PR
#[arg(long = "no-push")]
no_push: bool,
/// Mark the PR as a draft
#[arg(long)]
draft: bool,
/// Prefill the PR title and body from commits
#[arg(long)]
fill: bool,
/// Open the PR creation flow in the browser
#[arg(long)]
web: bool,
/// Remote to push the branch to before creating the PR
#[arg(long, default_value = "origin")]
remote: String,
/// Request reviews from the given GitHub handles
#[arg(long = "reviewer", value_name = "login")]
reviewers: Vec<String>,
/// Additional arguments passed directly to `gh pr create`
#[arg(last = true, value_name = "ARG")]
extra: Vec<String>,
}

pub fn run() -> color_eyre::Result<()> {
let cli = Cli::parse();
let repo = Repo::discover()?;
Expand All @@ -72,7 +108,60 @@ pub fn run() -> color_eyre::Result<()> {
let command = RemoveCommand::new(args.name, args.force);
command.execute(&repo)?;
}
Commands::PrGithub(args) => {
let worktree_name = resolve_worktree_name(args.name, &repo)?;
let mut command = PrGithubCommand::new(
worktree_name,
!args.no_push,
args.draft,
args.fill,
args.web,
args.remote,
args.reviewers,
args.extra,
);
command.execute(&repo)?;
}
}

Ok(())
}

fn resolve_worktree_name(name: Option<String>, repo: &Repo) -> color_eyre::Result<String> {
if let Some(name) = name {
return Ok(name);
}

let cwd = env::current_dir().wrap_err("failed to read current directory")?;
let canonical_cwd = cwd.canonicalize().unwrap_or(cwd);

let worktrees_dir = repo.ensure_worktrees_dir()?;
let canonical_worktrees_dir = worktrees_dir
.canonicalize()
.unwrap_or_else(|_| worktrees_dir.clone());

if !canonical_cwd.starts_with(&canonical_worktrees_dir) {
return Err(eyre::eyre!(
"`rsworktree pr-github` without <name> must be run from inside `{}`. Current directory: `{}`.",
worktrees_dir.display(),
canonical_cwd.display()
));
}

let relative = canonical_cwd
.strip_prefix(&canonical_worktrees_dir)
.wrap_err("failed to compute path relative to worktrees directory")?;

let components = relative
.components()
.map(|component| component.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>();

if components.is_empty() {
return Err(eyre::eyre!(
"Run `rsworktree pr-github` from inside a specific worktree (e.g. `.rsworktree/<name>`)."
));
}

Ok(components.join("/"))
}
5 changes: 3 additions & 2 deletions src/commands/cd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ impl CdCommand {
.as_str()
.if_supports_color(Stream::Stdout, |text| { format!("{}", text.blue().bold()) })
);
println!("Spawning shell in `{}`.", path);
let (program, args) = shell_command();
let mut cmd = Command::new(program);
println!("Spawning shell `{}` in `{}`...", program, path);

let mut cmd = Command::new(&program);
cmd.args(args);
cmd.current_dir(&canonical);
cmd.env("PWD", canonical.as_os_str());
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ pub mod cd;
pub mod create;
pub mod list;

pub mod pr_github;
pub mod rm;
Loading