diff --git a/crates/radicle-cli/examples/rad-patch-via-push.md b/crates/radicle-cli/examples/rad-patch-via-push.md index 951f2ea5..4abe8960 100644 --- a/crates/radicle-cli/examples/rad-patch-via-push.md +++ b/crates/radicle-cli/examples/rad-patch-via-push.md @@ -239,6 +239,90 @@ $ rad patch show 9580891 ╰────────────────────────────────────────────────────╯ ``` +## Detached HEAD + +In some cases, we may be creating patches from a detached HEAD state, but we +still want to have a tracking branch. We can do this using the `patch.branch` +option. + +``` +$ git commit --allow-empty -m "Going into detached HEAD" +[feature/2 831e838] Going into detached HEAD +``` + +``` (stderr) +$ git checkout 831e838 +Note: switching to '831e838'. + +You are in 'detached HEAD' state. You can look around, make experimental +changes and commit them, and you can discard any commits you make in this +state without impacting any branches by switching back to a branch. + +If you want to create a new branch to retain commits you create, you may +do so (now or later) by using -c with the switch command. Example: + + git switch -c + +Or undo this operation with: + + git switch - + +Turn off this advice by setting config variable advice.detachedHead to false + +HEAD is now at 831e838 Going into detached HEAD +$ git push rad HEAD:refs/patches -o patch.branch +✓ Patch e0fd879ac0a7d3ddcf3b440d8db656f5d7cebea3 opened +✓ Branch patches/e0fd879ac0a7d3ddcf3b440d8db656f5d7cebea3 created +hint: to update, run `git push rad patches/e0fd879ac0a7d3ddcf3b440d8db656f5d7cebea3` +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + * [new reference] HEAD -> refs/patches +``` + +The default name used for the branch is `patches/`. So let's checkout +the branch and push a new revision: + +``` (stderr) +$ git checkout patches/e0fd879ac0a7d3ddcf3b440d8db656f5d7cebea3 +Switched to branch 'patches/e0fd879ac0a7d3ddcf3b440d8db656f5d7cebea3' +$ git commit --allow-empty -m "Pushing new revision" +$ git push rad +✓ Patch e0fd879 updated to revision 943cbd9769e855d5e4eba419e68374c5141a2785 +To compare against your previous revision e0fd879, run: + + git range-diff [..] [..] [..] + +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + 831e838..d0ff2a1 patches/e0fd879ac0a7d3ddcf3b440d8db656f5d7cebea3 -> patches/e0fd879ac0a7d3ddcf3b440d8db656f5d7cebea3 +``` + +However, we also allow you to name the branch yourself: + +``` (stderr) +$ git checkout 831e838 -q +$ git push rad HEAD:refs/patches -o patch.branch='feature/3' +✓ Patch e0fd879ac0a7d3ddcf3b440d8db656f5d7cebea3 opened +✓ Branch feature/3 created +hint: to update, run `git push rad feature/3` +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + * [new reference] HEAD -> refs/patches +``` + +Let's checkout this branch and also push a new revision: + +``` (stderr) +$ git checkout feature/3 +Switched to branch 'feature/3' +$ git commit --allow-empty -m "Pushing new revision" +$ git push rad +✓ Patch e0fd879 updated to revision 943cbd9769e855d5e4eba419e68374c5141a2785 +To compare against your previous revision e0fd879, run: + + git range-diff [..] [..] [..] + +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + 831e838..d0ff2a1 feature/3 -> patches/e0fd879ac0a7d3ddcf3b440d8db656f5d7cebea3 +``` + ## Empty patch If we try to open a patch without making any changes to our base branch (`master`), diff --git a/crates/radicle-remote-helper/src/main.rs b/crates/radicle-remote-helper/src/main.rs index 2d3a62a8..3f5cd923 100644 --- a/crates/radicle-remote-helper/src/main.rs +++ b/crates/radicle-remote-helper/src/main.rs @@ -153,6 +153,35 @@ impl FromStr for Verbosity { } } +/// Branch creation options when creating a patch. +#[derive(Debug, Default, Clone)] +pub enum Branch { + /// Don't create a new branch. + #[default] + None, + /// Create a branch with the same name as the upstream branch (i.e. `patches/`). + MirrorUpstream, + /// Create a branch with the provided name. + Provided(git::RefString), +} + +impl Branch { + /// Return the branch name to be used for the local branch when creating a + /// patch. + pub fn to_branch_name(self, object: &radicle::patch::PatchId) -> Option { + match self { + Self::None => None, + Self::MirrorUpstream => Some(git::refs::patch(object)), + Self::Provided(name) => match name.clone().into_qualified() { + None => Some(git::fmt::lit::refs_heads(&name).into()), + // Ensure that if the reference is already qualified we do not + // add `refs/heads` + Some(name) => Some(name), + }, + } + } +} + #[derive(Debug, Default, Clone)] pub struct Options { /// Don't sync after push. @@ -167,6 +196,8 @@ pub struct Options { base: Option, /// Patch message. message: cli::patch::Message, + /// Create a branch and set its upstream when opening a patch. + branch: Branch, verbosity: Verbosity, } @@ -289,6 +320,7 @@ fn push_option(args: &[&str], opts: &mut Options) -> Result<(), Error> { ["sync.debug"] => opts.sync_debug = true, ["no-sync"] => opts.no_sync = true, ["patch.draft"] => opts.draft = true, + ["patch.branch"] => opts.branch = Branch::MirrorUpstream, _ => { let args = args.join(" "); @@ -315,6 +347,7 @@ fn push_option(args: &[&str], opts: &mut Options) -> Result<(), Error> { opts.base = Some(git::Oid::from(commit.id())); } + "patch.branch" => opts.branch = Branch::Provided(git::RefString::try_from(val)?), other => { return Err(Error::UnsupportedPushOption(other.to_owned())); } diff --git a/crates/radicle-remote-helper/src/push.rs b/crates/radicle-remote-helper/src/push.rs index daa65e7a..c5cc75f7 100644 --- a/crates/radicle-remote-helper/src/push.rs +++ b/crates/radicle-remote-helper/src/push.rs @@ -564,17 +564,52 @@ where )?; if let Some(upstream) = upstream { + if let Some(local_branch) = opts.branch.to_branch_name(&patch) { + fn strip_refs_heads(qualified: git::Qualified) -> git::RefString { + let (_refs, _heads, x, xs) = qualified.non_empty_components(); + std::iter::once(x).chain(xs).collect() + } + + working.reference( + &local_branch, + *head, + true, + "Create local branch for patch", + )?; + + let remote_branch = git::refs::workdir::patch_upstream(&patch); + let remote_branch = working.reference( + &remote_branch, + *head, + true, + "Create remote tracking branch for patch", + )?; + debug_assert!(remote_branch.is_remote()); + + let local_branch = strip_refs_heads(local_branch); + let upstream_branch = git::refs::patch(&patch); + git::set_upstream(working, upstream, &local_branch, &upstream_branch)?; + + eprintln!( + "{} Branch {} created", + term::PREFIX_SUCCESS, + term::format::tertiary(&local_branch), + ); + hint(format!( + "to update, run `git push {upstream} {local_branch}`" + )); + } // Setup current branch so that pushing updates the patch. - if let Some(branch) = + else if let Some(branch) = rad::setup_patch_upstream(&patch, head, working, upstream, false)? { if let Some(name) = branch.name()? { if profile.hints() { // Remove the remote portion of the name, i.e. // rad/patches/deadbeef -> patches/deadbeef - let name = name.split('/').skip(1).collect::>().join("/"); + let name = name.split_once('/').unwrap_or_default().1; hint(format!( - "to update, run `git push` or `git push rad -f HEAD:{name}`" + "to update, run `git push` or `git push {upstream} -f HEAD:{name}`" )); } } diff --git a/rad-patch.1.adoc b/rad-patch.1.adoc index 269ae129..0722a476 100644 --- a/rad-patch.1.adoc +++ b/rad-patch.1.adoc @@ -217,6 +217,11 @@ The full list of options follows: *patch.draft*:: Open the patch as a _draft_. Turned off by default. +*patch.branch[=]*:: + Create a branch when opening a patch. Turned off by default. + If a custom *name* is provided, this name is used. Otherwise + the branch name defaults to *patches/*. + *patch.message*=__:: To prevent the editor from opening, you can specify the patch message via this option. Multiple *patch.message* options are concatenated with a blank line