diff --git a/radicle-cli/examples/git/git-push-diverge.md b/radicle-cli/examples/git/git-push-diverge.md index bedc6c50..8d674e7f 100644 --- a/radicle-cli/examples/git/git-push-diverge.md +++ b/radicle-cli/examples/git/git-push-diverge.md @@ -65,3 +65,20 @@ $ git push rad To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi f2de534..f6cff86 master -> master ``` + +One thing of note is that we can't push an older commit either, by default: + +``` ~alice +$ git reset --hard HEAD^ -q +``` +``` ~alice (fail) +$ git push -f +``` + +We have to use the `allow.rollback` option: + +``` ~alice (stderr) +$ git push -f -o allow.rollback +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + + f6cff86...319a7dc master -> master (forced update) +``` diff --git a/radicle-remote-helper/src/lib.rs b/radicle-remote-helper/src/lib.rs index 713d3d5f..bfd3244f 100644 --- a/radicle-remote-helper/src/lib.rs +++ b/radicle-remote-helper/src/lib.rs @@ -38,6 +38,9 @@ pub enum Error { /// Invalid arguments received. #[error("invalid arguments: {0:?}")] InvalidArguments(Vec), + /// Unknown push option received. + #[error("unknown push option {0:?}")] + UnsupportedPushOption(String), /// Error with the remote url. #[error("invalid remote url: {0}")] RemoteUrl(#[from] UrlError), @@ -67,6 +70,11 @@ pub enum Error { List(#[from] list::Error), } +#[derive(Debug, Default, Clone)] +pub struct Allow { + rollback: bool, +} + #[derive(Debug, Default, Clone)] pub struct Options { /// Don't sync after push. @@ -77,6 +85,8 @@ pub struct Options { base: Option, /// Patch message. message: cli::patch::Message, + /// Operations allowed. + allow: Allow, } /// Run the radicle remote helper using the given profile. @@ -129,35 +139,11 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> { println!("ok"); } ["option", "push-option", args @ ..] => { - match *args { - ["sync"] => opts.no_sync = false, - ["no-sync"] => opts.no_sync = true, - ["patch.draft"] => opts.draft = true, - _ => { - let args = args.join(" "); - - if let Some((key, val)) = args.split_once('=') { - match key { - "patch.message" => { - opts.message.append(val); - } - "patch.base" => { - let base = cli::args::rev(&val.into()) - .map_err(|e| Error::Base(e.into()))?; - opts.base = Some(base); - } - _ => { - println!("unsupported"); - continue; - } - } - } else { - println!("unsupported"); - continue; - } - } + if push_option(args, &mut opts).is_ok() { + println!("ok"); + } else { + println!("unsupported"); } - println!("ok"); } ["option", "progress", ..] => { println!("unsupported"); @@ -204,6 +190,40 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> { } } +/// Parse a single push option. Returns `Ok` if it was successful. +/// Note that some push options can contain spaces, eg. `patch.message="Hello World!"`, +/// hence the arguments are passed as a slice. +fn push_option(args: &[&str], opts: &mut Options) -> Result<(), Error> { + match args { + ["sync"] => opts.no_sync = false, + ["no-sync"] => opts.no_sync = true, + ["patch.draft"] => opts.draft = true, + ["allow.rollback"] => opts.allow.rollback = true, + _ => { + let args = args.join(" "); + + if let Some((key, val)) = args.split_once('=') { + match key { + "patch.message" => { + opts.message.append(val); + } + "patch.base" => { + let base = + cli::args::rev(&val.into()).map_err(|e| Error::Base(e.into()))?; + opts.base = Some(base); + } + other => { + return Err(Error::UnsupportedPushOption(other.to_owned())); + } + } + } else { + return Err(Error::UnsupportedPushOption(args.to_owned())); + } + } + } + Ok(()) +} + /// Read one line from stdin, and split it into tokens. pub(crate) fn read_line<'a>(stdin: &io::Stdin, line: &'a mut String) -> io::Result> { line.clear(); diff --git a/radicle-remote-helper/src/push.rs b/radicle-remote-helper/src/push.rs index 57e3ca3f..db3c4c31 100644 --- a/radicle-remote-helper/src/push.rs +++ b/radicle-remote-helper/src/push.rs @@ -226,12 +226,21 @@ pub fn run( // If we're trying to update the canonical head, make sure // we don't diverge from the current head. + // + // Note that we *do* allow rolling back to a previous commit on the + // canonical branch. if dst == *canonical_ref && delegates.contains(&Did::from(nid)) { let head = working.find_reference(src.as_str())?; let head = head.peel_to_commit()?.id(); + // Rollback is allowed and head is an ancestor of the canonical head. + let rollback = opts.allow.rollback + && working.graph_descendant_of(**canonical_oid, head)?; if head != **canonical_oid + // Canonical head is *not* an ancestor of head. && !working.graph_descendant_of(head, **canonical_oid)? + // Not a rollback. + && !rollback { eprintln!( "hint: you are attempting to push a commit that would \