cob: Better distinguish patch revisions

The `Patch` API and semantics are changed a little bit:

* It's now valid again to propose a revision without being the patch
  author.
* To get revisions by the patch author, `Patch::updates` is used.
* `Patch::latest` now only looks at revisions by the patch author.
* Added `Patch::revisions_by`, `Patch::authors` and `Patch::latest_by`.
* The merge behavior on git-push is improved to favor revisions that
  merge more recent commits. We also allow for merging the non-latest
  revision.
This commit is contained in:
cloudhead 2023-09-21 13:46:08 +02:00
parent 8e58ba6433
commit c94bd50666
No known key found for this signature in database
5 changed files with 138 additions and 58 deletions

View File

@ -87,7 +87,13 @@ Fast-forward
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 README.md
create mode 100644 REQUIREMENTS.md
```
``` (stderr)
$ git push rad master
✓ Patch 69e881c606639691330051d7d8f013854f32fb87 merged at revision 70dd9a3
✓ Synced with 1 node(s)
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
f2de534..f567f69 master -> master
```
The patch is now merged and closed :).
@ -95,24 +101,22 @@ The patch is now merged and closed :).
```
$ rad patch show 69e881c
╭──────────────────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │
│ Patch 69e881c606639691330051d7d8f013854f32fb87 │
│ Author bob z6Mkt67…v4N1tRk │
│ Head f567f695d25b4e8fb63b5f5ad2a584529826e908 │
│ Branches master, patch/69e881c │
│ Commits up to date │
│ Status merged │
│ Title Define power requirements │
│ Patch 69e881c606639691330051d7d8f013854f32fb87 │
│ Author bob z6Mkt67…v4N1tRk │
│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
│ Commits ahead 0, behind 1 │
│ Status merged │
│ │
│ See details. │
├──────────────────────────────────────────────────────────────────────────────┤
│ f567f69 Use markdown for requirements │
│ 27857ec Add README, just for the fun │
│ 3e674d1 Define power requirements │
├──────────────────────────────────────────────────────────────────────────────┤
│ ● opened by bob z6Mkt67…v4N1tRk [ ... ]
│ ● opened by bob z6Mkt67…v4N1tRk [ ... ] │
│ ↑ updated to dcf3e6dd97c95cf8653cbb8ce47df20d28eb1821 (27857ec) [ ... ] │
↑ updated to 70dd9a31882d184a9fe8f1f590471f5543c4d85b (f567f69) [ ... ]
│ ✓ merged by alice (you) [ ... ]
* revised by alice (you) in 70dd9a3 (f567f69) [ ... ]
│ ✓ merged by alice (you) at revision 70dd9a3 (f567f69) [ ... ]
╰──────────────────────────────────────────────────────────────────────────────╯
```

View File

@ -138,33 +138,59 @@ pub fn timeline(
for (revision_id, revision) in patch.revisions() {
// Don't show an "update" line for the first revision.
if revision_id != root {
timeline.push((
revision.timestamp(),
term::Line::spaced(
[
term::format::tertiary("").into(),
term::format::default("updated to").into(),
term::format::dim(revision_id).into(),
term::format::parens(term::format::secondary(term::format::oid(
revision.head(),
)))
.into(),
]
.into_iter(),
),
));
if revision.author() == patch.author() {
timeline.push((
revision.timestamp(),
term::Line::spaced(
[
term::format::tertiary("").into(),
term::format::default("updated to").into(),
term::format::dim(revision_id).into(),
term::format::parens(term::format::secondary(term::format::oid(
revision.head(),
)))
.into(),
]
.into_iter(),
),
));
} else {
let (alias, nid) = Author::new(revision.author().id(), profile).labels();
timeline.push((
revision.timestamp(),
term::Line::spaced(
[
term::format::tertiary("*").into(),
term::format::default("revised by").into(),
alias.clone(),
nid.clone(),
term::format::default("in").into(),
term::format::dim(term::format::oid(revision_id)).into(),
term::format::parens(term::format::secondary(term::format::oid(
revision.head(),
)))
.into(),
]
.into_iter(),
),
));
}
}
for (nid, merge) in patch.merges().filter(|(_, m)| m.revision == revision_id) {
let peer = repository.remote(nid)?;
let (alias, nid) = Author::new(&peer.id, profile).labels();
let line = term::Line::spaced([
term::format::primary("").bold().into(),
term::format::default("merged").into(),
term::format::default("by").into(),
])
.space()
.extend(Author::new(&peer.id, profile).line());
term::format::default("merged by").into(),
alias,
nid,
term::format::default("at revision").into(),
term::format::dim(term::format::oid(merge.revision)).into(),
term::format::parens(term::format::secondary(term::format::oid(merge.commit)))
.into(),
]);
timeline.push((merge.timestamp, line));
}
for (reviewer, review) in revision.reviews() {

View File

@ -454,9 +454,10 @@ fn patch_merge_all<G: Signer>(
let mut revwalk = working.revwalk()?;
revwalk.push_range(&format!("{old}..{new}"))?;
// These commits are ordered by children first and then parents.
let commits = revwalk
.map(|r| r.map(git::Oid::from))
.collect::<Result<HashSet<git::Oid>, _>>()?;
.collect::<Result<Vec<git::Oid>, _>>()?;
let mut patches = patch::Patches::open(stored)?;
for patch in patches.all()? {
@ -464,12 +465,26 @@ fn patch_merge_all<G: Signer>(
// Skip patches that failed to load.
continue;
};
let (revision_id, revision) = patch.latest();
if !patch.is_open() {
continue;
}
// Later revisions are more likely to be merged, so we build the list backwards.
let revisions = patch
.revisions()
.rev()
.map(|(id, r)| (id, r.head()))
.collect::<Vec<_>>();
if patch.is_open() && commits.contains(&revision.head()) {
let patch = patch::PatchMut::new(id, patch, &mut patches);
// Try to find a revision to merge. Favor revisions that match the more recent commits.
// It's possible for more than one revision to be merged by this push, so we pick the
// revision that is closest to the tip of the commit chain we're pushing.
for commit in &commits {
if let Some((revision_id, _)) = revisions.iter().find(|(_, head)| commit == head) {
let patch = patch::PatchMut::new(id, patch, &mut patches);
patch_merge(patch, *revision_id, new, working, signer)?;
patch_merge(patch, revision_id, new, working, signer)?;
break;
}
}
}
Ok(())
@ -482,13 +497,23 @@ fn patch_merge<G: Signer>(
working: &git::raw::Repository,
signer: &G,
) -> Result<(), Error> {
let (latest, _) = patch.latest();
let merged = patch.merge(revision, commit, signer)?;
eprintln!(
"{} Patch {} merged",
cli::format::positive(""),
cli::format::tertiary(merged.patch)
);
if revision == latest {
eprintln!(
"{} Patch {} merged",
cli::format::positive(""),
cli::format::tertiary(merged.patch)
);
} else {
eprintln!(
"{} Patch {} merged at revision {}",
cli::format::positive(""),
cli::format::tertiary(merged.patch),
cli::format::oid(revision),
);
}
// Delete patch references that were created when the patch was opened.
// Note that we don't return an error if we can't delete the refs, since it's

View File

@ -11,7 +11,7 @@ use crate::prelude::*;
pub type Timestamp = LocalTime;
/// Author.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Author {
pub id: Did,
}

View File

@ -347,6 +347,8 @@ impl MergeTarget {
pub struct Patch {
/// Title of the patch.
pub(super) title: String,
/// Patch author.
pub(super) author: Author,
/// Current state of the patch.
pub(super) state: State,
/// Target this patch is meant to be merged in.
@ -380,6 +382,7 @@ impl Patch {
pub fn new(title: String, target: MergeTarget, (id, revision): (RevisionId, Revision)) -> Self {
Self {
title,
author: revision.author.clone(),
state: State::default(),
target,
labels: BTreeSet::default(),
@ -408,7 +411,7 @@ impl Patch {
/// Timestamp of the first revision of the patch.
pub fn timestamp(&self) -> Timestamp {
self.revisions()
self.updates()
.next()
.map(|(_, r)| r)
.expect("Patch::timestamp: at least one revision is present")
@ -428,12 +431,16 @@ impl Patch {
/// Author of the first revision of the patch.
pub fn author(&self) -> &Author {
&self
.revisions()
.next()
.map(|(_, r)| r)
.expect("Patch::author: at least one revision is present")
.author
&self.author
}
/// All revision authors.
pub fn authors(&self) -> BTreeSet<&Author> {
self.revisions
.values()
.filter_map(|r| r.as_ref())
.map(|r| &r.author)
.collect()
}
/// Get the `Revision` by its `RevisionId`.
@ -443,10 +450,15 @@ impl Patch {
self.revisions.get(id).and_then(|o| o.as_ref())
}
/// List of patch revisions. The initial changeset is part of the
/// List of patch revisions by the patch author. The initial changeset is part of the
/// first revision.
pub fn updates(&self) -> impl DoubleEndedIterator<Item = (RevisionId, &Revision)> {
self.revisions_by(self.author().public_key())
}
/// List of all patch revisions by all authors.
pub fn revisions(&self) -> impl DoubleEndedIterator<Item = (RevisionId, &Revision)> {
self.timeline.iter().filter_map(|id| {
self.timeline.iter().filter_map(move |id| {
self.revisions
.get(id)
.and_then(|o| o.as_ref())
@ -454,6 +466,15 @@ impl Patch {
})
}
/// List of patch revisions by the given author.
pub fn revisions_by<'a>(
&'a self,
author: &'a PublicKey,
) -> impl DoubleEndedIterator<Item = (RevisionId, &Revision)> {
self.revisions()
.filter(move |(_, r)| (r.author.public_key() == author))
}
/// List of patch assignees.
pub fn assignees(&self) -> impl Iterator<Item = Did> + '_ {
self.assignees.iter().map(Did::from)
@ -504,18 +525,22 @@ impl Patch {
///
/// This is the revision that was created with the patch.
pub fn root(&self) -> (RevisionId, &Revision) {
self.revisions()
self.updates()
.next()
.expect("Patch::root: there is always a root revision")
}
/// Latest revision.
/// Latest revision by the patch author.
pub fn latest(&self) -> (RevisionId, &Revision) {
self.revisions()
.next_back()
self.latest_by(self.author().public_key())
.expect("Patch::latest: there is always at least one revision")
}
/// Latest revision by the given author.
pub fn latest_by<'a>(&'a self, author: &'a PublicKey) -> Option<(RevisionId, &Revision)> {
self.revisions_by(author).next_back()
}
/// Time of last update.
pub fn updated_at(&self) -> Timestamp {
self.latest().1.timestamp()
@ -618,8 +643,8 @@ impl Patch {
// Redacted.
Authorization::Unknown
}
// Only patch authors can propose revisions.
Action::Revision { .. } => Authorization::from(actor == author),
// Anyone can propose revisions.
Action::Revision { .. } => Authorization::Allow,
// Only the revision author can edit or redact their revision.
Action::RevisionEdit { revision, .. } | Action::RevisionRedact { revision, .. } => {
if let Some(revision) = lookup::revision(self, revision)? {