Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Patch message from all commits
Slack Coder committed 2 years ago
commit 92648f934e270a076b6f3867a039060bcf8cd9bb
parent 520fb61230f1eca9bb3dceacfc7229fb24758647
7 files changed +385 -86
modified radicle-cli/examples/rad-merge-via-push.md
@@ -9,10 +9,10 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
 * [new reference]   HEAD -> refs/patches
```
``` (stderr) RAD_SOCKET=/dev/null
-
$ git checkout -b feature/2 -q
+
$ git checkout -b feature/2 -q master
$ git commit --allow-empty -q -m "Second change"
$ git push rad HEAD:refs/patches
-
✓ Patch bf923942537708f7aec4680a321be68cf933e747 opened
+
✓ Patch 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0 opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```
@@ -23,7 +23,7 @@ This creates some remote tracking branches for us:
$ git branch -r
  rad/master
  rad/patches/0ec956c94256fa101db4c32956ce195a1aa0edf2
-
  rad/patches/bf923942537708f7aec4680a321be68cf933e747
+
  rad/patches/928d76e22ef98a8406f2e4e4bcc8878533bbdfe0
```

And some remote refs:
@@ -36,12 +36,12 @@ $ rad inspect --refs
        |-- cobs
        |   `-- xyz.radicle.patch
        |       |-- 0ec956c94256fa101db4c32956ce195a1aa0edf2
-
        |       `-- bf923942537708f7aec4680a321be68cf933e747
+
        |       `-- 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0
        |-- heads
        |   |-- master
        |   `-- patches
        |       |-- 0ec956c94256fa101db4c32956ce195a1aa0edf2
-
        |       `-- bf923942537708f7aec4680a321be68cf933e747
+
        |       `-- 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0
        `-- rad
            |-- id
            `-- sigrefs
@@ -61,9 +61,9 @@ When we push to `rad/master`, we automatically merge the patches:
``` (stderr) RAD_SOCKET=/dev/null
$ git push rad master
✓ Patch 0ec956c94256fa101db4c32956ce195a1aa0edf2 merged
-
✓ Patch bf923942537708f7aec4680a321be68cf933e747 merged
+
✓ Patch 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0 merged
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
   f2de534..e9fff34  master -> master
+
   f2de534..d6399c7  master -> master
```
```
$ rad patch --merged
@@ -71,7 +71,7 @@ $ rad patch --merged
│ ●  ID       Title          Author                  Head     +   -   Updated      │
├──────────────────────────────────────────────────────────────────────────────────┤
│ ✔  0ec956c  First change   z6MknSL…StBU8Vi  (you)  20aa5dd  +0  -0  [   ...    ] │
-
│ ✔  bf92394  Second change  z6MknSL…StBU8Vi  (you)  e9fff34  +0  -0  [   ...    ] │
+
│ ✔  928d76e  Second change  z6MknSL…StBU8Vi  (you)  daf349f  +0  -0  [   ...    ] │
╰──────────────────────────────────────────────────────────────────────────────────╯
```

@@ -92,7 +92,7 @@ $ rad inspect --refs
        |-- cobs
        |   `-- xyz.radicle.patch
        |       |-- 0ec956c94256fa101db4c32956ce195a1aa0edf2
-
        |       `-- bf923942537708f7aec4680a321be68cf933e747
+
        |       `-- 928d76e22ef98a8406f2e4e4bcc8878533bbdfe0
        |-- heads
        |   `-- master
        `-- rad
modified radicle-cli/examples/rad-patch-via-push.md
@@ -72,10 +72,10 @@ We can also create patches by pushing to the `rad/patches` remote. It's a bit
simpler:

``` (stderr)
-
$ git checkout -b feature/2 -q
+
$ git checkout -b feature/2 -q master
$ git commit -a -m "Add more things" -q --allow-empty
$ git push rad/patches
-
✓ Patch 2af090f48003d86f735163794bfffdb2691f369e opened
+
✓ Patch b8ab1c99c1c8205680a3494f04fb3934ec738ddd opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```
@@ -85,7 +85,7 @@ We see both branches with upstreams now:
```
$ git branch -vv
  feature/1 42d894a [rad/patches/2647168c23e7c2b2c1936d695443944e143bc3f7] Add things
-
* feature/2 b94a835 [rad/patches/2af090f48003d86f735163794bfffdb2691f369e] Add more things
+
* feature/2 8b0ea80 [rad/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd] Add more things
  master    f2de534 [rad/master] Second commit
```

@@ -97,7 +97,7 @@ $ rad patch
│ ●  ID       Title            Author                  Head     +   -   Updated      │
├────────────────────────────────────────────────────────────────────────────────────┤
│ ●  2647168  Add things #1    z6MknSL…StBU8Vi  (you)  42d894a  +0  -0  [    ...   ] │
-
│ ●  2af090f  Add more things  z6MknSL…StBU8Vi  (you)  b94a835  +0  -0  [    ...   ] │
+
│ ●  b8ab1c9  Add more things  z6MknSL…StBU8Vi  (you)  8b0ea80  +0  -0  [    ...   ] │
╰────────────────────────────────────────────────────────────────────────────────────╯
```

@@ -116,9 +116,9 @@ $ git commit -a -m "Improve code" -q --allow-empty

``` (stderr)
$ git push
-
✓ Patch 2af090f updated to f532e40e44de298b27d2255acb50b99bf0377a04
+
✓ Patch b8ab1c9 updated to 8767880c31b9e4a04cdb07ad6faa9ce453980399
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
   b94a835..662843e  feature/2 -> patches/2af090f48003d86f735163794bfffdb2691f369e
+
   8b0ea80..02bef3f  feature/2 -> patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd
```

This last `git push` worked without specifying an upstream branch despite the
@@ -136,22 +136,21 @@ This allows for pushing to the remote patch branch without using the full
We can then see that the patch head has moved:

```
-
$ rad patch show 2af090f
+
$ rad patch show b8ab1c9
╭──────────────────────────────────────────────────────────────────────────────╮
│ Title     Add more things                                                    │
-
│ Patch     2af090f48003d86f735163794bfffdb2691f369e                           │
+
│ Patch     b8ab1c99c1c8205680a3494f04fb3934ec738ddd                           │
│ Author    did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi           │
-
│ Head      662843ed81e76efa69d7901fb7bdd775043015d0                           │
+
│ Head      02bef3fac41b2f98bb3c02b868a53ddfecb55b5f                           │
│ Branches  feature/2                                                          │
-
│ Commits   ahead 3, behind 0                                                  │
+
│ Commits   ahead 2, behind 0                                                  │
│ Status    open                                                               │
├──────────────────────────────────────────────────────────────────────────────┤
-
│ 662843e Improve code                                                         │
-
│ b94a835 Add more things                                                      │
-
│ 42d894a Add things                                                           │
+
│ 02bef3f Improve code                                                         │
+
│ 8b0ea80 Add more things                                                      │
├──────────────────────────────────────────────────────────────────────────────┤
│ ● opened by (you) (z6MknSL…StBU8Vi) [   ...    ]                             │
-
│ ↑ updated to f532e40e44de298b27d2255acb50b99bf0377a04 (662843e) [   ...    ] │
+
│ ↑ updated to 8767880c31b9e4a04cdb07ad6faa9ce453980399 (02bef3f) [   ...    ] │
╰──────────────────────────────────────────────────────────────────────────────╯
```

@@ -159,19 +158,19 @@ And we can check that all the refs are properly updated in our repository:

```
$ git rev-parse HEAD
-
662843ed81e76efa69d7901fb7bdd775043015d0
+
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f
```

```
$ git status --short --branch
-
## feature/2...rad/patches/2af090f48003d86f735163794bfffdb2691f369e
+
## feature/2...rad/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd
```

```
-
$ git rev-parse refs/remotes/rad/patches/2af090f48003d86f735163794bfffdb2691f369e
-
662843ed81e76efa69d7901fb7bdd775043015d0
-
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/patches/2af090f48003d86f735163794bfffdb2691f369e
-
662843ed81e76efa69d7901fb7bdd775043015d0	refs/heads/patches/2af090f48003d86f735163794bfffdb2691f369e
+
$ git rev-parse refs/remotes/rad/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd
+
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f
+
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd
+
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f	refs/heads/patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd
```

## Force push
@@ -183,7 +182,7 @@ Let's try.

```
$ git commit --amend -m "Amended commit" --allow-empty
-
[feature/2 3507cd5] Amended commit
+
[feature/2 9304dbc] Amended commit
 Date: [..]
```

@@ -192,7 +191,7 @@ Now let's push to the patch head.
``` (stderr) (fail)
$ git push
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
 ! [rejected]        feature/2 -> patches/2af090f48003d86f735163794bfffdb2691f369e (non-fast-forward)
+
 ! [rejected]        feature/2 -> patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd (non-fast-forward)
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
hint: Updates were rejected because a pushed branch tip is behind its remote
hint: counterpart. Check out this branch and integrate the remote changes
@@ -205,30 +204,29 @@ use `--force` to force the update.

``` (stderr)
$ git push --force
-
✓ Patch 2af090f updated to d7590abe04594263eeb88cc0e28502139ec8414f
+
✓ Patch b8ab1c9 updated to f24334f8cea7b7a5bcaf3bc6deb1408c9bf507ad
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
 + 662843e...3507cd5 feature/2 -> patches/2af090f48003d86f735163794bfffdb2691f369e (forced update)
+
 + 02bef3f...9304dbc feature/2 -> patches/b8ab1c99c1c8205680a3494f04fb3934ec738ddd (forced update)
```

That worked. We can see the new revision if we call `rad patch show`:

```
-
$ rad patch show 2af090f
+
$ rad patch show b8ab1c9
╭──────────────────────────────────────────────────────────────────────────────╮
│ Title     Add more things                                                    │
-
│ Patch     2af090f48003d86f735163794bfffdb2691f369e                           │
+
│ Patch     b8ab1c99c1c8205680a3494f04fb3934ec738ddd                           │
│ Author    did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi           │
-
│ Head      3507cd57811fe5f21f6a0a610a1ac8068b3a5d9f                           │
+
│ Head      9304dbc445925187994a7a93222a3f8bde73b785                           │
│ Branches  feature/2                                                          │
-
│ Commits   ahead 3, behind 0                                                  │
+
│ Commits   ahead 2, behind 0                                                  │
│ Status    open                                                               │
├──────────────────────────────────────────────────────────────────────────────┤
-
│ 3507cd5 Amended commit                                                       │
-
│ b94a835 Add more things                                                      │
-
│ 42d894a Add things                                                           │
+
│ 9304dbc Amended commit                                                       │
+
│ 8b0ea80 Add more things                                                      │
├──────────────────────────────────────────────────────────────────────────────┤
│ ● opened by (you) (z6MknSL…StBU8Vi) [   ...    ]                             │
-
│ ↑ updated to f532e40e44de298b27d2255acb50b99bf0377a04 (662843e) [   ...    ] │
-
│ ↑ updated to d7590abe04594263eeb88cc0e28502139ec8414f (3507cd5) [   ...    ] │
+
│ ↑ updated to 8767880c31b9e4a04cdb07ad6faa9ce453980399 (02bef3f) [   ...    ] │
+
│ ↑ updated to f24334f8cea7b7a5bcaf3bc6deb1408c9bf507ad (9304dbc) [   ...    ] │
╰──────────────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/src/commands/patch/create.rs
@@ -1,5 +1,3 @@
-
use anyhow::anyhow;
-

use radicle::cob::patch;
use radicle::git;
use radicle::node::Handle;
@@ -81,13 +79,10 @@ pub fn run(
    // TODO: List matching working copy refs for all targets.

    let head_oid = branch_oid(&head_branch)?;
-
    let head_commit = storage.backend.find_commit(*head_oid)?;
-
    let head_commit_msg = head_commit
-
        .message()
-
        .ok_or(anyhow!("commit summary is not valid UTF-8; aborting"))?;
-
    let (title, description) = term::patch::get_message(message, head_commit_msg)?;
-

    let base_oid = storage.backend.merge_base(*target_oid, *head_oid)?;
+
    let (title, description) =
+
        term::patch::get_create_message(message, &storage.backend, &base_oid.into(), &head_oid)?;
+

    let signer = term::signer(profile)?;
    let patch = if draft {
        patches.draft(
modified radicle-cli/src/commands/patch/edit.rs
@@ -18,8 +18,7 @@ pub fn run(
        anyhow::bail!("Patch `{patch_id}` not found");
    };

-
    let default_msg = term::patch::message(patch.title(), patch.description());
-
    let (title, description) = term::patch::get_message(message, &default_msg)?;
+
    let (title, description) = term::patch::get_edit_message(message, &patch)?;

    let title = if title != patch.title() {
        Some(title)
modified radicle-cli/src/commands/patch/update.rs
@@ -92,7 +92,7 @@ pub fn run(

    let head_oid = branch_oid(&head_branch)?;
    let base_oid = storage.backend.merge_base(*target_oid, *head_oid)?;
-
    let message = term::patch::get_update_message(message)?;
+
    let message = term::patch::get_update_message(message, workdir, current_revision, &head_oid)?;
    let signer = term::signer(profile)?;
    let revision = patch.update(message, base_oid, *head_oid, &signer)?;

modified radicle-cli/src/terminal/patch.rs
@@ -1,11 +1,29 @@
+
use std::fmt;
+
use std::fmt::Write;
use std::io;
use std::io::IsTerminal as _;

+
use thiserror::Error;
+

+
use radicle::cob;
+
use radicle::cob::patch;
use radicle::git;

use crate::terminal as term;
use crate::terminal::Element;

+
#[derive(Debug, Error)]
+
pub enum Error {
+
    #[error(transparent)]
+
    Fmt(#[from] fmt::Error),
+
    #[error("git: {0}")]
+
    Git(#[from] git::raw::Error),
+
    #[error("i/o error: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("invalid utf-8 string")]
+
    InvalidUtf8,
+
}
+

/// The user supplied `Patch` description.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Message {
@@ -78,17 +96,91 @@ pub fn message(title: &str, description: &str) -> String {
    format!("{title}\n\n{description}").trim().to_string()
}

+
/// Create a helpful default `Patch` message out of one or more commit messages.
+
fn message_from_commits(name: &str, commits: Vec<git::raw::Commit>) -> Result<String, Error> {
+
    let mut commits = commits.into_iter().rev();
+
    let count = commits.len();
+
    let Some(commit) = commits.next() else {
+
        return Ok(String::default());
+
    };
+
    let commit_msg = commit.message().ok_or(Error::InvalidUtf8)?.to_string();
+

+
    if count == 1 {
+
        return Ok(commit_msg);
+
    }
+

+
    // Many commits
+
    let mut msg = String::new();
+
    writeln!(&mut msg, "<!--")?;
+
    writeln!(
+
        &mut msg,
+
        "This {name} is the combination of {count} commits.",
+
    )?;
+
    writeln!(&mut msg, "This is the first commit message:")?;
+
    writeln!(&mut msg, "-->")?;
+
    writeln!(&mut msg)?;
+
    writeln!(&mut msg, "{commit_msg}")?;
+
    writeln!(&mut msg)?;
+

+
    for (i, commit) in commits.enumerate() {
+
        let commit_msg = commit.message().ok_or(Error::InvalidUtf8)?;
+
        let commit_num = i + 2;
+

+
        writeln!(&mut msg, "<!--")?;
+
        writeln!(&mut msg, "This is commit message #{commit_num}:")?;
+
        writeln!(&mut msg, "-->")?;
+
        writeln!(&mut msg)?;
+
        writeln!(&mut msg, "{commit_msg}")?;
+
        writeln!(&mut msg)?;
+
    }
+

+
    Ok(msg.trim().to_string())
+
}
+

+
/// Return commits between the merge base and a head.
+
pub fn patch_commits<'a>(
+
    repo: &'a git::raw::Repository,
+
    base: &git::Oid,
+
    head: &git::Oid,
+
) -> Result<Vec<git::raw::Commit<'a>>, git::raw::Error> {
+
    let mut commits = Vec::new();
+
    let mut revwalk = repo.revwalk()?;
+
    revwalk.push_range(&format!("{base}..{head}"))?;
+

+
    for rev in revwalk {
+
        let commit = repo.find_commit(rev?)?;
+
        commits.push(commit);
+
    }
+
    Ok(commits)
+
}
+

+
/// The message shown in the editor when creating a `Patch`.
+
fn create_display_message(
+
    repo: &git::raw::Repository,
+
    base: &git::Oid,
+
    head: &git::Oid,
+
) -> Result<String, Error> {
+
    let commits = patch_commits(repo, base, head)?;
+
    if commits.is_empty() {
+
        return Ok(PATCH_MSG.trim_start().to_string());
+
    }
+

+
    let summary = message_from_commits("patch", commits)?;
+
    Ok(format!("{summary}\n{PATCH_MSG}"))
+
}
+

/// Get the Patch title and description from the command line arguments, or request it from the
/// user.
///
/// The user can bail out if an empty title is entered.
-
pub fn get_message(
+
pub fn get_create_message(
    message: term::patch::Message,
-
    default_msg: &str,
-
) -> io::Result<(String, String)> {
-
    let display_msg = default_msg.trim_end();
-

-
    let message = message.get(&format!("{display_msg}\n{PATCH_MSG}"))?;
+
    repo: &git::raw::Repository,
+
    base: &git::Oid,
+
    head: &git::Oid,
+
) -> Result<(String, String), Error> {
+
    let display_msg = create_display_message(repo, base, head)?;
+
    let message = message.get(&display_msg)?;

    let (title, description) = message.split_once('\n').unwrap_or((&message, ""));
    let (title, description) = (title.trim().to_string(), description.trim().to_string());
@@ -97,15 +189,72 @@ pub fn get_message(
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "a patch title must be provided",
+
        )
+
        .into());
+
    }
+

+
    Ok((title, description))
+
}
+

+
/// The message shown in the editor when editing a `Patch`.
+
fn edit_display_message(title: &str, description: &str) -> String {
+
    format!("{}\n\n{}\n{PATCH_MSG}", title, description)
+
        .trim_start()
+
        .to_string()
+
}
+

+
/// Get a patch edit message.
+
pub fn get_edit_message(
+
    patch_message: term::patch::Message,
+
    patch: &cob::patch::Patch,
+
) -> io::Result<(String, String)> {
+
    let display_msg = edit_display_message(patch.title(), patch.description());
+
    let patch_message = patch_message.get(&display_msg)?;
+
    let patch_message = patch_message.replace(PATCH_MSG.trim(), ""); // Delete help message.
+

+
    let (title, description) = patch_message
+
        .split_once('\n')
+
        .unwrap_or((&patch_message, ""));
+
    let (title, description) = (title.trim().to_string(), description.trim().to_string());
+

+
    if title.is_empty() {
+
        return Err(io::Error::new(
+
            io::ErrorKind::InvalidInput,
+
            "a patch title must be provided",
        ));
    }

    Ok((title, description))
}

+
/// The message shown in the editor when updating a `Patch`.
+
fn update_display_message(
+
    repo: &git::raw::Repository,
+
    last_rev_head: &git::Oid,
+
    head: &git::Oid,
+
) -> Result<String, Error> {
+
    if !repo.graph_descendant_of(**head, **last_rev_head)? {
+
        return Ok(REVISION_MSG.trim_start().to_string());
+
    }
+

+
    let commits = patch_commits(repo, last_rev_head, head)?;
+
    if commits.is_empty() {
+
        return Ok(REVISION_MSG.trim_start().to_string());
+
    }
+

+
    let summary = message_from_commits("patch", commits)?;
+
    Ok(format!("{summary}\n{REVISION_MSG}"))
+
}
+

/// Get a patch update message.
-
pub fn get_update_message(message: term::patch::Message) -> io::Result<String> {
-
    let message = message.get(REVISION_MSG)?;
+
pub fn get_update_message(
+
    message: term::patch::Message,
+
    repo: &git::raw::Repository,
+
    latest: &patch::Revision,
+
    head: &git::Oid,
+
) -> Result<String, Error> {
+
    let display_msg = update_display_message(repo, &latest.head(), head)?;
+
    let message = message.get(&display_msg)?;
    let message = message.trim();

    Ok(message.to_owned())
@@ -152,34 +301,182 @@ pub fn print_commits_ahead_behind(
#[cfg(test)]
mod test {
    use super::*;
+
    use radicle::git::refname;
+
    use radicle::test::fixtures;
+
    use std::path;

-
    #[test]
-
    fn test_get_message() {
-
        let res = get_message(
-
            Message::Text("title\n\ndescription".to_string()),
-
            "default text",
+
    fn commit(
+
        repo: &git::raw::Repository,
+
        branch: &git::RefStr,
+
        parent: &git::Oid,
+
        msg: &str,
+
    ) -> git::Oid {
+
        let sig = git::raw::Signature::new(
+
            "anonymous",
+
            "anonymous@radicle.xyz",
+
            &git::raw::Time::new(0, 0),
        )
        .unwrap();
-
        assert_eq!(("title".to_string(), "description".to_string()), res);
+
        let head = repo.find_commit(**parent).unwrap();
+
        let tree =
+
            git::write_tree(path::Path::new("README"), "Hello World!\n".as_bytes(), repo).unwrap();

-
        let res = get_message(
-
            Message::Text("title\ndescription\nsecond description".to_string()),
-
            "default text",
-
        )
-
        .unwrap();
+
        let branch = git::refs::branch(branch);
+
        let commit = git::commit(repo, &head, &branch, msg, &sig, &tree).unwrap();
+

+
        commit.id().into()
+
    }
+

+
    #[test]
+
    fn test_create_display_message() {
+
        let tmpdir = tempfile::tempdir().unwrap();
+
        let (repo, commit_0) = fixtures::repository(&tmpdir);
+
        let commit_0 = commit_0.into();
+
        let commit_1 = commit(&repo, &refname!("feature"), &commit_0, "commit 1");
+
        let commit_2 = commit(&repo, &refname!("feature"), &commit_1, "commit 2");
+

+
        let res = create_display_message(&repo, &commit_0, &commit_0).unwrap();
        assert_eq!(
-
            (
-
                "title".to_string(),
-
                "description\nsecond description".to_string()
-
            ),
+
            r#"
+
<!--
+
Please enter a patch message for your changes. An empty
+
message aborts the patch proposal.
+

+
The first line is the patch title. The patch description
+
follows, and must be separated with a blank line, just
+
like a commit message. Markdown is supported in the title
+
and description.
+
-->
+
"#
+
            .trim_start(),
            res
        );

-
        let res = get_message(
-
            Message::Text(" title \ndescription  \n \n ".to_string()),
-
            "default text",
-
        )
-
        .unwrap();
-
        assert_eq!(("title".to_string(), "description".to_string()), res);
+
        let res = create_display_message(&repo, &commit_0, &commit_1).unwrap();
+
        assert_eq!(
+
            r#"
+
commit 1
+

+
<!--
+
Please enter a patch message for your changes. An empty
+
message aborts the patch proposal.
+

+
The first line is the patch title. The patch description
+
follows, and must be separated with a blank line, just
+
like a commit message. Markdown is supported in the title
+
and description.
+
-->
+
"#
+
            .trim_start(),
+
            res
+
        );
+

+
        let res = create_display_message(&repo, &commit_0, &commit_2).unwrap();
+
        assert_eq!(
+
            r#"
+
<!--
+
This patch is the combination of 2 commits.
+
This is the first commit message:
+
-->
+

+
commit 1
+

+
<!--
+
This is commit message #2:
+
-->
+

+
commit 2
+

+
<!--
+
Please enter a patch message for your changes. An empty
+
message aborts the patch proposal.
+

+
The first line is the patch title. The patch description
+
follows, and must be separated with a blank line, just
+
like a commit message. Markdown is supported in the title
+
and description.
+
-->
+
"#
+
            .trim_start(),
+
            res
+
        );
+
    }
+

+
    #[test]
+
    fn test_edit_display_message() {
+
        let res = edit_display_message("title", "The patch description.");
+
        assert_eq!(
+
            r#"
+
title
+

+
The patch description.
+

+
<!--
+
Please enter a patch message for your changes. An empty
+
message aborts the patch proposal.
+

+
The first line is the patch title. The patch description
+
follows, and must be separated with a blank line, just
+
like a commit message. Markdown is supported in the title
+
and description.
+
-->
+
"#
+
            .trim_start(),
+
            res
+
        );
+
    }
+

+
    #[test]
+
    fn test_update_display_message() {
+
        let tmpdir = tempfile::tempdir().unwrap();
+
        let (repo, commit_0) = fixtures::repository(&tmpdir);
+
        let commit_0 = commit_0.into();
+

+
        let commit_1 = commit(&repo, &refname!("feature"), &commit_0, "commit 1");
+
        let commit_2 = commit(&repo, &refname!("feature"), &commit_1, "commit 2");
+
        let commit_squashed = commit(
+
            &repo,
+
            &refname!("squashed-feature"),
+
            &commit_0,
+
            "commit squashed",
+
        );
+

+
        let res = update_display_message(&repo, &commit_1, &commit_1).unwrap();
+
        assert_eq!(
+
            r#"
+
<!--
+
Please enter a comment for your patch update. Leaving this
+
blank is also okay.
+
-->
+
"#
+
            .trim_start(),
+
            res
+
        );
+

+
        let res = update_display_message(&repo, &commit_1, &commit_2).unwrap();
+
        assert_eq!(
+
            r#"
+
commit 2
+

+
<!--
+
Please enter a comment for your patch update. Leaving this
+
blank is also okay.
+
-->
+
"#
+
            .trim_start(),
+
            res
+
        );
+

+
        let res = update_display_message(&repo, &commit_1, &commit_squashed).unwrap();
+
        assert_eq!(
+
            r#"
+
<!--
+
Please enter a comment for your patch update. Leaving this
+
blank is also okay.
+
-->
+
"#
+
            .trim_start(),
+
            res
+
        );
    }
}
modified radicle-remote-helper/src/push.rs
@@ -65,6 +65,9 @@ pub enum Error {
    /// Patch COB error.
    #[error(transparent)]
    Patch(#[from] radicle::cob::patch::Error),
+
    /// Patch edit message error.
+
    #[error(transparent)]
+
    PatchEdit(#[from] cli::patch::Error),
    /// Patch not found in store.
    #[error("patch `{0}` not found")]
    NotFound(patch::PatchId),
@@ -254,11 +257,12 @@ fn patch_open<G: Signer>(
    // not fail, since the reference will already exist with the correct OID.
    push_ref(src, &dst, false, working, stored.raw())?;

-
    let mut patches = patch::Patches::open(stored)?;
-
    let fallback = commit.message().unwrap_or_default();
-
    let (title, description) = cli::patch::get_message(msg, fallback)?;
    let (_, target) = stored.canonical_head()?;
    let base = stored.backend.merge_base(*target, commit.id())?;
+
    let (title, description) =
+
        cli::patch::get_create_message(msg, &stored.backend, &base.into(), &commit.id().into())?;
+

+
    let mut patches = patch::Patches::open(stored)?;
    let result = match patches.create(
        &title,
        &description,
@@ -361,7 +365,13 @@ fn patch_update<G: Signer>(
    if patch.revisions().any(|(_, r)| *r.head() == commit.id()) {
        return Ok(());
    }
-
    let message = cli::patch::get_update_message(msg)?;
+
    let message = cli::patch::get_update_message(
+
        msg,
+
        &stored.backend,
+
        patch.latest().1,
+
        &commit.id().into(),
+
    )?;
+

    let (_, target) = stored.canonical_head()?;
    let base = stored.backend.merge_base(*target, commit.id())?;
    let revision = patch.update(message, base, commit.id(), signer)?;