remote-helper: Allow commit roll-backs on push

This makes the divergence check on push slightly more lenient, by
allowing rolling back to a previous commit in the canonical branch
history.

Without this, it can become impossible to roll-back commits in certain
situations.
This commit is contained in:
cloudhead 2023-11-07 12:45:24 +01:00
parent 193ed2f675
commit 823a7e7c52
No known key found for this signature in database
3 changed files with 74 additions and 28 deletions

View File

@ -65,3 +65,20 @@ $ git push rad
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
f2de534..f6cff86 master -> master 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)
```

View File

@ -38,6 +38,9 @@ pub enum Error {
/// Invalid arguments received. /// Invalid arguments received.
#[error("invalid arguments: {0:?}")] #[error("invalid arguments: {0:?}")]
InvalidArguments(Vec<String>), InvalidArguments(Vec<String>),
/// Unknown push option received.
#[error("unknown push option {0:?}")]
UnsupportedPushOption(String),
/// Error with the remote url. /// Error with the remote url.
#[error("invalid remote url: {0}")] #[error("invalid remote url: {0}")]
RemoteUrl(#[from] UrlError), RemoteUrl(#[from] UrlError),
@ -67,6 +70,11 @@ pub enum Error {
List(#[from] list::Error), List(#[from] list::Error),
} }
#[derive(Debug, Default, Clone)]
pub struct Allow {
rollback: bool,
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct Options { pub struct Options {
/// Don't sync after push. /// Don't sync after push.
@ -77,6 +85,8 @@ pub struct Options {
base: Option<Rev>, base: Option<Rev>,
/// Patch message. /// Patch message.
message: cli::patch::Message, message: cli::patch::Message,
/// Operations allowed.
allow: Allow,
} }
/// Run the radicle remote helper using the given profile. /// Run the radicle remote helper using the given profile.
@ -129,35 +139,11 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> {
println!("ok"); println!("ok");
} }
["option", "push-option", args @ ..] => { ["option", "push-option", args @ ..] => {
match *args { if push_option(args, &mut opts).is_ok() {
["sync"] => opts.no_sync = false, println!("ok");
["no-sync"] => opts.no_sync = true, } else {
["patch.draft"] => opts.draft = true, println!("unsupported");
_ => {
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;
}
}
} }
println!("ok");
} }
["option", "progress", ..] => { ["option", "progress", ..] => {
println!("unsupported"); 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. /// 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<Vec<&'a str>> { pub(crate) fn read_line<'a>(stdin: &io::Stdin, line: &'a mut String) -> io::Result<Vec<&'a str>> {
line.clear(); line.clear();

View File

@ -226,12 +226,21 @@ pub fn run(
// If we're trying to update the canonical head, make sure // If we're trying to update the canonical head, make sure
// we don't diverge from the current head. // 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)) { if dst == *canonical_ref && delegates.contains(&Did::from(nid)) {
let head = working.find_reference(src.as_str())?; let head = working.find_reference(src.as_str())?;
let head = head.peel_to_commit()?.id(); 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 if head != **canonical_oid
// Canonical head is *not* an ancestor of head.
&& !working.graph_descendant_of(head, **canonical_oid)? && !working.graph_descendant_of(head, **canonical_oid)?
// Not a rollback.
&& !rollback
{ {
eprintln!( eprintln!(
"hint: you are attempting to push a commit that would \ "hint: you are attempting to push a commit that would \