canonical: Support Annotated Tags
The logic for computing canonical references conflated the semantics of annotated and lighweight tags, yielding confusing/wrong results. The main culprit was the call to `peel_to_commit` in `impl ReadRepository for Repository`, peeling tag objects away. To resolve this, we separate out quroum computation per object type: one implementation for commits, and one for tags. Also move move canonical error types and methods into their own module to have a cleaner file structure for the main logic. Capture the `BTreeMap<Oid, u8>` type into a `Votes` struct, with its own API. We were getting the objects from the repository twice – once in `Canonical::new` and once again in `ensure_commit_or_tag` – so the latter was removed. Use an `enum CanonicalObject` to specify which object types the canonical process supports. During `converges`, separate commits and tags, and ensure that we are only looking at objects of one type. Note, that Fintan think this is actually the *wrong* place to do it because we already filter the objects – so we should keep track of that information higher up the callstack. Co-authored-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
parent
54fd8c40a0
commit
3d352f23e6
|
|
@ -21,7 +21,7 @@ $ git commit --amend -m "Neue Änderungen" --allow-empty -q
|
||||||
|
|
||||||
``` ~alice (stderr)
|
``` ~alice (stderr)
|
||||||
$ git push rad master -f
|
$ git push rad master -f
|
||||||
✓ Canonical reference refs/heads/master updated to 9170c8795d3a78f0381a0ffafb20ea69fb0f5b6b
|
✓ Canonical reference refs/heads/master updated to target commit 9170c8795d3a78f0381a0ffafb20ea69fb0f5b6b
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
+ fb25886...9170c87 master -> master (forced update)
|
+ fb25886...9170c87 master -> master (forced update)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
In this example, we will show how we can make other references become canonical.
|
||||||
|
To illustrate, we will use annotated Git tags as an example. The storage of the repository
|
||||||
|
should look something like this by the end of the example:
|
||||||
|
|
||||||
|
~~~
|
||||||
|
storage/z6cFWeWpnZNHh9rUW8phgA3b5yGt/refs
|
||||||
|
├── heads
|
||||||
|
│ └── main
|
||||||
|
├── namespaces
|
||||||
|
│ ├── z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
│ │ └── refs
|
||||||
|
│ │ ├── cobs
|
||||||
|
│ │ │ └── xyz.radicle.id
|
||||||
|
│ │ │ └── 865c48204bd7bb7f088b8db90ffdccb48cfa0a50
|
||||||
|
│ │ ├── heads
|
||||||
|
│ │ │ └── master
|
||||||
|
│ │ ├── tags
|
||||||
|
│ │ │ ├── v1.0-hotfix
|
||||||
|
│ │ │ └── v1.0
|
||||||
|
│ │ └── rad
|
||||||
|
│ │ ├── id
|
||||||
|
│ │ └── sigrefs
|
||||||
|
│ └── z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
|
│ └── refs
|
||||||
|
│ ├── heads
|
||||||
|
│ │ └── master
|
||||||
|
│ ├── tags
|
||||||
|
│ │ ├── v1.0-hotfix
|
||||||
|
│ │ └── v1.0
|
||||||
|
│ └── rad
|
||||||
|
│ ├── id
|
||||||
|
│ └── sigrefs
|
||||||
|
├── rad
|
||||||
|
│ └── id
|
||||||
|
└── tags
|
||||||
|
├── v1.0-hotfix
|
||||||
|
└── v1.0
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Noting that there are tags under `refs/tags` now.
|
||||||
|
|
||||||
|
To start, Alice will add a new payload to the repository identity. The
|
||||||
|
identifier for this payload is `xyz.radicle.crefs`. It contains a single field
|
||||||
|
with the key `rules`, and the value for this key is an array of rules. In this
|
||||||
|
case, we will have two rules: one for `refs/tags/*` and one for `refs/tags/qa/*`
|
||||||
|
(see RIP-0004 for more information on the rules).
|
||||||
|
|
||||||
|
``` ~alice
|
||||||
|
$ rad id update --title "Add canonical reference rules" --payload xyz.radicle.crefs rules '{ "refs/tags/*": { "threshold": 1, "allow": "delegates" }, "refs/tags/qa/*": { "threshold": 1, "allow": "delegates" }}'
|
||||||
|
✓ Identity revision [..] created
|
||||||
|
╭────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ Title Add canonical reference rules │
|
||||||
|
│ Revision c3349f07bfe6a82bbeb2989d2de4a918408f9831 │
|
||||||
|
│ Blob 85fa09e2de93b825d5231778dbb34143004a4bca │
|
||||||
|
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||||
|
│ State accepted │
|
||||||
|
│ Quorum yes │
|
||||||
|
├────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
|
||||||
|
╰────────────────────────────────────────────────────────────────────────╯
|
||||||
|
|
||||||
|
@@ -1,13 +1,25 @@
|
||||||
|
{
|
||||||
|
"payload": {
|
||||||
|
+ "xyz.radicle.crefs": {
|
||||||
|
+ "rules": {
|
||||||
|
+ "refs/tags/*": {
|
||||||
|
+ "allow": "delegates",
|
||||||
|
+ "threshold": 1
|
||||||
|
+ },
|
||||||
|
+ "refs/tags/qa/*": {
|
||||||
|
+ "allow": "delegates",
|
||||||
|
+ "threshold": 1
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ },
|
||||||
|
"xyz.radicle.project": {
|
||||||
|
"defaultBranch": "master",
|
||||||
|
"description": "Radicle Heartwood Protocol & Stack",
|
||||||
|
"name": "heartwood"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delegates": [
|
||||||
|
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||||
|
],
|
||||||
|
"threshold": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, Alice will create an annotated tag and push it:
|
||||||
|
|
||||||
|
``` ~alice
|
||||||
|
$ git tag -a -m "Hotfix for release 1" v1.0-hotfix
|
||||||
|
$ git cat-file -t v1.0-hotfix
|
||||||
|
tag
|
||||||
|
```
|
||||||
|
|
||||||
|
``` ~alice (stderr)
|
||||||
|
$ git push rad --tags
|
||||||
|
✓ Canonical reference refs/tags/v1.0-hotfix updated to target tag ac51a0746a5e8311829bc481202909a1e3acc0c2
|
||||||
|
✓ Synced with 1 seed(s)
|
||||||
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
* [new tag] v1.0-hotfix -> v1.0-hotfix
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that the output included a message about a canonical reference being
|
||||||
|
updated:
|
||||||
|
|
||||||
|
~~~
|
||||||
|
✓ Canonical reference refs/tags/v1.0-hotfix updated to target tag ac51a0746a5e8311829bc481202909a1e3acc0c2
|
||||||
|
~~~
|
||||||
|
|
||||||
|
On the other side, Bob performs a fetch and now has the tags locally:
|
||||||
|
|
||||||
|
``` ~bob (stderr)
|
||||||
|
$ cd heartwood
|
||||||
|
$ git fetch rad
|
||||||
|
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||||
|
* [new tag] v1.0-hotfix -> rad/tags/v1.0-hotfix
|
||||||
|
* [new tag] v1.0-hotfix -> v1.0-hotfix
|
||||||
|
```
|
||||||
|
|
||||||
|
Since Alice crated an annotated tag, resolving the reference on Bob's end yields an object of type 'tag'.
|
||||||
|
|
||||||
|
``` ~bob
|
||||||
|
$ git cat-file -t v1.0-hotfix
|
||||||
|
tag
|
||||||
|
```
|
||||||
|
|
||||||
|
In the next portion of this example, we want to show that using a `threshold` of
|
||||||
|
`2` requires both delegates. To do this, Bob creates a `master` reference, Alice
|
||||||
|
adds him as a remote, and adds him to the identity delegates, as well as setting
|
||||||
|
the `threshold` to `2` for the `refs/tags/*` rule:
|
||||||
|
|
||||||
|
``` ~bob
|
||||||
|
$ rad remote add z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --name alice
|
||||||
|
✓ Follow policy updated for z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
|
||||||
|
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 1 potential seed(s).
|
||||||
|
✓ Target met: 1 seed(s)
|
||||||
|
✓ Remote alice added
|
||||||
|
✓ Remote-tracking branch alice/master created for z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
$ git push rad master
|
||||||
|
```
|
||||||
|
|
||||||
|
``` ~alice
|
||||||
|
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
|
||||||
|
✓ Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
|
||||||
|
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 1 potential seed(s).
|
||||||
|
✓ Target met: 1 seed(s)
|
||||||
|
✓ Remote bob added
|
||||||
|
✓ Remote-tracking branch bob/master created for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
|
$ rad id update --title "Add Bob" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm -q
|
||||||
|
27ab0d77a95581c59ca9d30e679ceb06a9f758db
|
||||||
|
$ rad id update --title "Update canonical reference rules" --payload xyz.radicle.crefs rules '{ "refs/tags/*": { "threshold": 2, "allow": "delegates" }, "refs/tags/qa/*": { "threshold": 1, "allow": "delegates" } }' -q
|
||||||
|
dace164ba43fa51802697ec28d0b1965a9d7808b
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** here we have to specify all the rules again to update the `threshold`.
|
||||||
|
In reality, you can use `rad id update --edit` and edit the payload in your
|
||||||
|
editor instead.
|
||||||
|
|
||||||
|
``` ~bob
|
||||||
|
$ rad sync -f
|
||||||
|
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 1 potential seed(s).
|
||||||
|
✓ Target met: 1 seed(s)
|
||||||
|
🌱 Fetched from z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
$ rad id accept dace164ba43fa51802697ec28d0b1965a9d7808b -q
|
||||||
|
```
|
||||||
|
|
||||||
|
When Bob creates a new tag and pushes it, we see that there's a warning that
|
||||||
|
no quorum was found for the new tag:
|
||||||
|
|
||||||
|
``` ~bob
|
||||||
|
$ git tag -l
|
||||||
|
v1.0-hotfix
|
||||||
|
$ git rev-parse v1.0-hotfix
|
||||||
|
ac51a0746a5e8311829bc481202909a1e3acc0c2
|
||||||
|
```
|
||||||
|
|
||||||
|
``` ~bob (stderr)
|
||||||
|
$ git tag -a -m "Release 2" v2.0
|
||||||
|
$ git push rad --tags
|
||||||
|
warn: could not determine target for canonical reference 'refs/tags/v2.0', no object with at least 2 vote(s) found (threshold not met)
|
||||||
|
warn: it is recommended to find an object (either commit or tag) to agree upon
|
||||||
|
✓ Synced with 1 seed(s)
|
||||||
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
|
* [new tag] v1.0-hotfix -> v1.0-hotfix
|
||||||
|
* [new tag] v2.0 -> v2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Alice can then fetch and checkout the new tag, create one on her side, and push
|
||||||
|
it:
|
||||||
|
|
||||||
|
``` ~alice (stderr)
|
||||||
|
$ git fetch bob
|
||||||
|
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
|
* [new tag] v1.0-hotfix -> bob/tags/v1.0-hotfix
|
||||||
|
* [new tag] v2.0 -> bob/tags/v2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
At this point Alice might check out `v2.0` and consider whether she agrees with Bob.
|
||||||
|
Let's say that Alice agrees, so she copies the tag to her repository using `git tag`:
|
||||||
|
|
||||||
|
``` ~alice
|
||||||
|
$ git tag v2.0 bob/tags/v2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
``` ~alice (stderr)
|
||||||
|
$ git push rad --tags
|
||||||
|
✓ Canonical reference refs/tags/v2.0 updated to target tag 89f935f27a16f8ed97915ade4accab8fe48057aa
|
||||||
|
✓ Synced with 1 seed(s)
|
||||||
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
|
* [new tag] v2.0 -> v2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that Bob has also pushed this tag, we can see that the tag was made
|
||||||
|
canonical.
|
||||||
|
|
||||||
|
For the final portion of the example, we will show that both delegates aren't
|
||||||
|
required for pushing tags that match the rule `refs/tags/qa/*`. To show this,
|
||||||
|
Bob will create a lightweight tag and push it, and we should see that the canonical
|
||||||
|
reference is created:
|
||||||
|
|
||||||
|
``` ~bob (stderr)
|
||||||
|
$ git tag qa/v2.1
|
||||||
|
$ git push rad --tags
|
||||||
|
✓ Canonical reference refs/tags/qa/v2.1 updated to target commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||||
|
✓ Synced with 1 seed(s)
|
||||||
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
|
* [new tag] qa/v2.1 -> qa/v2.1
|
||||||
|
```
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
In this example, we will show how we can make other references become canonical.
|
In this example, we will show how we can make other references become canonical.
|
||||||
To illustrate, we will use Git tags as an example. The storage of the repository
|
To illustrate, we will use lightweight Git tags as an example. The storage of the repository
|
||||||
should look something like this by the end of the example:
|
should look something like this by the end of the example:
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
|
|
@ -95,7 +95,7 @@ $ git tag v1.0-hotfix
|
||||||
|
|
||||||
``` ~alice (stderr)
|
``` ~alice (stderr)
|
||||||
$ git push rad --tags
|
$ git push rad --tags
|
||||||
✓ Canonical reference refs/tags/v1.0-hotfix updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
✓ Canonical reference refs/tags/v1.0-hotfix updated to target commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
* [new tag] v1.0-hotfix -> v1.0-hotfix
|
* [new tag] v1.0-hotfix -> v1.0-hotfix
|
||||||
|
|
@ -105,7 +105,7 @@ Notice that the output included a message about a canonical reference being
|
||||||
updated:
|
updated:
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
✓ Canonical reference refs/tags/v1.0-hotfix updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
✓ Canonical reference refs/tags/v1.0-hotfix updated to target commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
On the other side, Bob performs a fetch and now has the tags locally:
|
On the other side, Bob performs a fetch and now has the tags locally:
|
||||||
|
|
@ -118,6 +118,13 @@ From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
||||||
* [new tag] v1.0-hotfix -> v1.0-hotfix
|
* [new tag] v1.0-hotfix -> v1.0-hotfix
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Since Alice crated a lightweight tag, resolving the reference on Bob's end yields an object of type 'commit'.
|
||||||
|
|
||||||
|
``` ~bob
|
||||||
|
$ git cat-file -t v1.0-hotfix
|
||||||
|
commit
|
||||||
|
```
|
||||||
|
|
||||||
In the next portion of this example, we want to show that using a `threshold` of
|
In the next portion of this example, we want to show that using a `threshold` of
|
||||||
`2` requires both delegates. To do this, Bob creates a `master` reference, Alice
|
`2` requires both delegates. To do this, Bob creates a `master` reference, Alice
|
||||||
adds him as a remote, and adds him to the identity delegates, as well as setting
|
adds him as a remote, and adds him to the identity delegates, as well as setting
|
||||||
|
|
@ -164,8 +171,8 @@ no quorum was found for the new tag:
|
||||||
``` ~bob (stderr)
|
``` ~bob (stderr)
|
||||||
$ git tag v2.0
|
$ git tag v2.0
|
||||||
$ git push rad --tags
|
$ git push rad --tags
|
||||||
warn: could not determine commit for canonical reference 'refs/tags/v2.0', no commit with at least 2 vote(s) found (threshold not met)
|
warn: could not determine target for canonical reference 'refs/tags/v2.0', no object with at least 2 vote(s) found (threshold not met)
|
||||||
warn: it is recommended to find a commit to agree upon
|
warn: it is recommended to find an object (either commit or tag) to agree upon
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
* [new tag] v1.0-hotfix -> v1.0-hotfix
|
* [new tag] v1.0-hotfix -> v1.0-hotfix
|
||||||
|
|
@ -182,14 +189,16 @@ From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WR
|
||||||
* [new tag] v2.0 -> bob/tags/v2.0
|
* [new tag] v2.0 -> bob/tags/v2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
At this point Alice might check out `v2.0` and consider whether she agrees with Bob.
|
||||||
|
Let's say that Alice agrees, so she copies the tag to her repository using `git tag`:
|
||||||
|
|
||||||
``` ~alice
|
``` ~alice
|
||||||
$ git checkout bob/tags/v2.0
|
$ git tag v2.0 bob/tags/v2.0
|
||||||
$ git tag v2.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
``` ~alice (stderr)
|
``` ~alice (stderr)
|
||||||
$ git push rad --tags
|
$ git push rad --tags
|
||||||
✓ Canonical reference refs/tags/v2.0 updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
✓ Canonical reference refs/tags/v2.0 updated to target commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
* [new tag] v2.0 -> v2.0
|
* [new tag] v2.0 -> v2.0
|
||||||
|
|
@ -206,7 +215,7 @@ reference is created:
|
||||||
``` ~bob (stderr)
|
``` ~bob (stderr)
|
||||||
$ git tag qa/v2.1
|
$ git tag qa/v2.1
|
||||||
$ git push rad --tags
|
$ git push rad --tags
|
||||||
✓ Canonical reference refs/tags/qa/v2.1 updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
✓ Canonical reference refs/tags/qa/v2.1 updated to target commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
* [new tag] qa/v2.1 -> qa/v2.1
|
* [new tag] qa/v2.1 -> qa/v2.1
|
||||||
|
|
@ -91,8 +91,8 @@ commit:
|
||||||
|
|
||||||
``` ~alice (stderr)
|
``` ~alice (stderr)
|
||||||
$ git push rad -f
|
$ git push rad -f
|
||||||
warn: could not determine commit for canonical reference 'refs/heads/master', no commit with at least 3 vote(s) found (threshold not met)
|
warn: could not determine target for canonical reference 'refs/heads/master', no object with at least 3 vote(s) found (threshold not met)
|
||||||
warn: it is recommended to find a commit to agree upon
|
warn: it is recommended to find an object (either commit or tag) to agree upon
|
||||||
✓ Synced with 2 seed(s)
|
✓ Synced with 2 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
d09e634..0f9bd80 master -> master
|
d09e634..0f9bd80 master -> master
|
||||||
|
|
@ -116,7 +116,7 @@ become the canonical `master`.
|
||||||
|
|
||||||
``` ~bob (stderr)
|
``` ~bob (stderr)
|
||||||
$ git push rad
|
$ git push rad
|
||||||
✓ Canonical reference refs/heads/master updated to 3a75f66dd0020c9a0355cc6ec21f15de989e2001
|
✓ Canonical reference refs/heads/master updated to target commit 3a75f66dd0020c9a0355cc6ec21f15de989e2001
|
||||||
✓ Synced with 2 seed(s)
|
✓ Synced with 2 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
2a37862..0f9bd80 master -> master
|
2a37862..0f9bd80 master -> master
|
||||||
|
|
@ -137,7 +137,7 @@ HEAD is now at 0f9bd80 Merge remote-tracking branch 'eve/master'
|
||||||
|
|
||||||
``` ~eve (stderr)
|
``` ~eve (stderr)
|
||||||
$ git push rad
|
$ git push rad
|
||||||
✓ Canonical reference refs/heads/master updated to 0f9bd8035c04b3f73f5408e73e8454879b20800b
|
✓ Canonical reference refs/heads/master updated to target commit 0f9bd8035c04b3f73f5408e73e8454879b20800b
|
||||||
✓ Synced with 2 seed(s)
|
✓ Synced with 2 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
|
||||||
3a75f66..0f9bd80 master -> master
|
3a75f66..0f9bd80 master -> master
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ f2de534 Second commit
|
||||||
```
|
```
|
||||||
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
||||||
$ git push rad
|
$ git push rad
|
||||||
✓ Canonical reference refs/heads/master updated to f6cff86594495e9beccfeda7c20173e55c1dd9fc
|
✓ Canonical reference refs/heads/master updated to target commit f6cff86594495e9beccfeda7c20173e55c1dd9fc
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..f6cff86 master -> master
|
f2de534..f6cff86 master -> master
|
||||||
```
|
```
|
||||||
|
|
@ -74,7 +74,7 @@ $ git reset --hard HEAD^ -q
|
||||||
```
|
```
|
||||||
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
||||||
$ git push -f
|
$ git push -f
|
||||||
✓ Canonical reference refs/heads/master updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
✓ Canonical reference refs/heads/master updated to target commit 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
+ f6cff86...319a7dc master -> master (forced update)
|
+ f6cff86...319a7dc master -> master (forced update)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ Fast-forward
|
||||||
|
|
||||||
``` ~alice (stderr)
|
``` ~alice (stderr)
|
||||||
$ git push rad
|
$ git push rad
|
||||||
✓ Canonical reference refs/heads/master updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
✓ Canonical reference refs/heads/master updated to target commit 319a7dc3b195368ded4b099f8c90bbb80addccd3
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..319a7dc master -> master
|
f2de534..319a7dc master -> master
|
||||||
|
|
@ -54,7 +54,7 @@ push and the new canonical head becomes the previous commit again:
|
||||||
|
|
||||||
``` ~alice (stderr)
|
``` ~alice (stderr)
|
||||||
$ git push rad -f
|
$ git push rad -f
|
||||||
✓ Canonical reference refs/heads/master updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
✓ Canonical reference refs/heads/master updated to target commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
+ 319a7dc...f2de534 master -> master (forced update)
|
+ 319a7dc...f2de534 master -> master (forced update)
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ $ git commit -v -m "Define power requirements"
|
||||||
|
|
||||||
``` ~alice (stderr) RAD_SOCKET=/dev/null
|
``` ~alice (stderr) RAD_SOCKET=/dev/null
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Canonical reference refs/heads/master updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
✓ Canonical reference refs/heads/master updated to target commit 3e674d1a1df90807e934f9ae5da2591dd6848a33
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..3e674d1 master -> master
|
f2de534..3e674d1 master -> master
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ $ git commit --amend --allow-empty -q -m "Amended change"
|
||||||
$ git checkout master -q
|
$ git checkout master -q
|
||||||
$ git merge feature/1 -q
|
$ git merge feature/1 -q
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Canonical reference refs/heads/master updated to 954bcdb5008447ce294a61a21d7eb87afbe7f4a6
|
✓ Canonical reference refs/heads/master updated to target commit 954bcdb5008447ce294a61a21d7eb87afbe7f4a6
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..954bcdb master -> master
|
f2de534..954bcdb master -> master
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ Finally, we push master and expect the patch to be merged.
|
||||||
``` (stderr) RAD_SOCKET=/dev/null
|
``` (stderr) RAD_SOCKET=/dev/null
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
||||||
✓ Canonical reference refs/heads/master updated to 737a10cfa29111afeb0d43cf3545cee386b939ec
|
✓ Canonical reference refs/heads/master updated to target commit 737a10cfa29111afeb0d43cf3545cee386b939ec
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..737a10c master -> master
|
f2de534..737a10c master -> master
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ When we push to `rad/master`, we automatically merge the patches:
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch 356f73863a8920455ff6e77cd9c805d68910551b merged
|
✓ Patch 356f73863a8920455ff6e77cd9c805d68910551b merged
|
||||||
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
||||||
✓ Canonical reference refs/heads/master updated to d6399c71702b40bae00825b3c444478d06b4e91c
|
✓ Canonical reference refs/heads/master updated to target commit d6399c71702b40bae00825b3c444478d06b4e91c
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..d6399c7 master -> master
|
f2de534..d6399c7 master -> master
|
||||||
```
|
```
|
||||||
|
|
@ -148,7 +148,7 @@ the first patch, even though they were pushed together.
|
||||||
$ git reset --hard HEAD^
|
$ git reset --hard HEAD^
|
||||||
$ git push -f rad
|
$ git push -f rad
|
||||||
! Patch 356f73863a8920455ff6e77cd9c805d68910551b reverted at revision 356f738
|
! Patch 356f73863a8920455ff6e77cd9c805d68910551b reverted at revision 356f738
|
||||||
✓ Canonical reference refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
✓ Canonical reference refs/heads/master updated to target commit 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
+ d6399c7...20aa5dd master -> master (forced update)
|
+ d6399c7...20aa5dd master -> master (forced update)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ $ git checkout master -q
|
||||||
$ git merge feature/1
|
$ git merge feature/1
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch 8dfb4dcafc4346158c8160410dd3f2b0616ad4fe merged
|
✓ Patch 8dfb4dcafc4346158c8160410dd3f2b0616ad4fe merged
|
||||||
✓ Canonical reference refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
✓ Canonical reference refs/heads/master updated to target commit 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..20aa5dd master -> master
|
f2de534..20aa5dd master -> master
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ $ git checkout master -q
|
||||||
$ git merge changes -q
|
$ git merge changes -q
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch acab0ec777a97d013f30be5d5d1aec32562ecb02 merged
|
✓ Patch acab0ec777a97d013f30be5d5d1aec32562ecb02 merged
|
||||||
✓ Canonical reference refs/heads/master updated to b2b6432af93f8fe188e32d400263021b602cfec8
|
✓ Canonical reference refs/heads/master updated to target commit b2b6432af93f8fe188e32d400263021b602cfec8
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
|
|
||||||
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/tree/b2b6432af93f8fe188e32d400263021b602cfec8
|
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/tree/b2b6432af93f8fe188e32d400263021b602cfec8
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Switched to branch 'master'
|
||||||
$ git merge feature/1
|
$ git merge feature/1
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
|
||||||
✓ Canonical reference refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
|
✓ Canonical reference refs/heads/master updated to target commit 20aa5dde6210796c3a2f04079b42316a31d02689
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..20aa5dd master -> master
|
f2de534..20aa5dd master -> master
|
||||||
```
|
```
|
||||||
|
|
@ -50,7 +50,7 @@ When pushing, notice that we're told our patch is reverted.
|
||||||
``` (stderr) RAD_SOCKET=/dev/null
|
``` (stderr) RAD_SOCKET=/dev/null
|
||||||
$ git push rad master --force
|
$ git push rad master --force
|
||||||
! Patch 696ec5508494692899337afe6713fe1796d0315c reverted at revision 696ec55
|
! Patch 696ec5508494692899337afe6713fe1796d0315c reverted at revision 696ec55
|
||||||
✓ Canonical reference refs/heads/master updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
✓ Canonical reference refs/heads/master updated to target commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
+ 20aa5dd...f2de534 master -> master (forced update)
|
+ 20aa5dd...f2de534 master -> master (forced update)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ Fast-forward
|
||||||
``` (stderr)
|
``` (stderr)
|
||||||
$ git push rad master
|
$ git push rad master
|
||||||
✓ Patch e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46 merged at revision 9d62420
|
✓ Patch e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46 merged at revision 9d62420
|
||||||
✓ Canonical reference refs/heads/master updated to f567f695d25b4e8fb63b5f5ad2a584529826e908
|
✓ Canonical reference refs/heads/master updated to target commit f567f695d25b4e8fb63b5f5ad2a584529826e908
|
||||||
✓ Synced with 1 seed(s)
|
✓ Synced with 1 seed(s)
|
||||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||||
f2de534..f567f69 master -> master
|
f2de534..f567f69 master -> master
|
||||||
|
|
|
||||||
|
|
@ -2571,7 +2571,7 @@ fn git_tag() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn git_push_canonical_tags() {
|
fn git_push_canonical_lightweight_tags() {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
let alice = environment.node("alice");
|
let alice = environment.node("alice");
|
||||||
let bob = environment.node("bob");
|
let bob = environment.node("bob");
|
||||||
|
|
@ -2595,7 +2595,49 @@ fn git_push_canonical_tags() {
|
||||||
bob.clone(rid, environment.work(&bob)).unwrap();
|
bob.clone(rid, environment.work(&bob)).unwrap();
|
||||||
formula(
|
formula(
|
||||||
&environment.tempdir(),
|
&environment.tempdir(),
|
||||||
"examples/git/git-push-canonical-tags.md",
|
"examples/git/git-push-canonical-lightweight-tags.md",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.home(
|
||||||
|
"alice",
|
||||||
|
environment.work(&alice),
|
||||||
|
[("RAD_HOME", alice.home.path().display())],
|
||||||
|
)
|
||||||
|
.home(
|
||||||
|
"bob",
|
||||||
|
environment.work(&bob),
|
||||||
|
[("RAD_HOME", bob.home.path().display())],
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn git_push_canonical_annotated_tags() {
|
||||||
|
let mut environment = Environment::new();
|
||||||
|
let alice = environment.node("alice");
|
||||||
|
let bob = environment.node("bob");
|
||||||
|
|
||||||
|
let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
|
||||||
|
|
||||||
|
fixtures::repository(environment.work(&alice));
|
||||||
|
|
||||||
|
test(
|
||||||
|
"examples/rad-init.md",
|
||||||
|
environment.work(&alice),
|
||||||
|
Some(&alice.home),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let alice = alice.spawn();
|
||||||
|
let mut bob = bob.spawn();
|
||||||
|
|
||||||
|
bob.connect(&alice).converge([&alice]);
|
||||||
|
bob.clone(rid, environment.work(&bob)).unwrap();
|
||||||
|
formula(
|
||||||
|
&environment.tempdir(),
|
||||||
|
"examples/git/git-push-canonical-annotated-tags.md",
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.home(
|
.home(
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ pub enum Error {
|
||||||
Address(#[from] address::Error),
|
Address(#[from] address::Error),
|
||||||
/// A service error.
|
/// A service error.
|
||||||
#[error("service error: {0}")]
|
#[error("service error: {0}")]
|
||||||
Service(#[from] service::Error),
|
Service(Box<service::Error>),
|
||||||
/// An I/O error.
|
/// An I/O error.
|
||||||
#[error("i/o error: {0}")]
|
#[error("i/o error: {0}")]
|
||||||
Io(#[from] io::Error),
|
Io(#[from] io::Error),
|
||||||
|
|
@ -92,6 +92,12 @@ pub enum Error {
|
||||||
GitVersion(#[from] git::VersionError),
|
GitVersion(#[from] git::VersionError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<service::Error> for Error {
|
||||||
|
fn from(e: service::Error) -> Self {
|
||||||
|
Self::Service(Box::new(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Wraps a [`UnixListener`] but tracks its origin.
|
/// Wraps a [`UnixListener`] but tracks its origin.
|
||||||
pub enum ControlSocket {
|
pub enum ControlSocket {
|
||||||
/// The listener was created by binding to it.
|
/// The listener was created by binding to it.
|
||||||
|
|
|
||||||
|
|
@ -122,9 +122,9 @@ impl Handle {
|
||||||
log::trace!(target: "worker", "Set HEAD to {}", head.new);
|
log::trace!(target: "worker", "Set HEAD to {}", head.new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(RepositoryError::Quorum(radicle::git::canonical::QuorumError::Git(e))) => {
|
Err(RepositoryError::Quorum(
|
||||||
return Err(e.into())
|
radicle::git::canonical::error::QuorumError::Git(e),
|
||||||
}
|
)) => return Err(e.into()),
|
||||||
Err(RepositoryError::Quorum(e)) => {
|
Err(RepositoryError::Quorum(e)) => {
|
||||||
log::warn!(target: "worker", "Fetch could not set HEAD: {e}")
|
log::warn!(target: "worker", "Fetch could not set HEAD: {e}")
|
||||||
}
|
}
|
||||||
|
|
@ -406,7 +406,7 @@ fn set_canonical_refs(repo: &Repository, applied: &Applied) -> Result<(), error:
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Ok((refname, oid)) => {
|
Ok((refname, _, oid)) => {
|
||||||
if let Err(e) = repo.backend.reference(
|
if let Err(e) = repo.backend.reference(
|
||||||
refname.clone().as_str(),
|
refname.clone().as_str(),
|
||||||
*oid,
|
*oid,
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,13 @@ pub enum Error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Repository(#[from] radicle::storage::RepositoryError),
|
Repository(#[from] radicle::storage::RepositoryError),
|
||||||
#[error("namespaces error: {0}")]
|
#[error("namespaces error: {0}")]
|
||||||
Namespaces(#[from] NamespacesError),
|
Namespaces(Box<NamespacesError>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NamespacesError> for Error {
|
||||||
|
fn from(e: NamespacesError) -> Self {
|
||||||
|
Self::Namespaces(Box::new(e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
|
@ -304,7 +310,13 @@ enum TryFetchError<'a> {
|
||||||
#[error("peer fetch capacity reached; cannot initiate fetch")]
|
#[error("peer fetch capacity reached; cannot initiate fetch")]
|
||||||
SessionCapacityReached,
|
SessionCapacityReached,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Namespaces(#[from] NamespacesError),
|
Namespaces(Box<NamespacesError>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NamespacesError> for TryFetchError<'_> {
|
||||||
|
fn from(e: NamespacesError) -> Self {
|
||||||
|
Self::Namespaces(Box::new(e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch state for an ongoing fetch.
|
/// Fetch state for an ongoing fetch.
|
||||||
|
|
|
||||||
|
|
@ -113,13 +113,17 @@ pub enum Error {
|
||||||
Repository(#[from] radicle::storage::RepositoryError),
|
Repository(#[from] radicle::storage::RepositoryError),
|
||||||
/// Quorum error.
|
/// Quorum error.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Quorum(#[from] radicle::git::canonical::QuorumError),
|
Quorum(#[from] radicle::git::canonical::error::QuorumError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
CanonicalRefs(#[from] radicle::identity::doc::CanonicalRefsError),
|
CanonicalRefs(#[from] radicle::identity::doc::CanonicalRefsError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
PushAction(#[from] error::PushAction),
|
PushAction(#[from] error::PushAction),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Canonical(#[from] error::CanonicalUnrecoverable),
|
Canonical(#[from] error::CanonicalUnrecoverable),
|
||||||
|
#[error(transparent)]
|
||||||
|
CanonicalInit(#[from] radicle::git::canonical::error::CanonicalError),
|
||||||
|
#[error("could not determine object type for {oid}")]
|
||||||
|
UnknownObjectType { oid: git::Oid },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push command.
|
/// Push command.
|
||||||
|
|
@ -280,7 +284,8 @@ pub fn run(
|
||||||
let identity = stored.identity()?;
|
let identity = stored.identity()?;
|
||||||
let project = identity.project()?;
|
let project = identity.project()?;
|
||||||
let canonical_ref = git::refs::branch(project.default_branch());
|
let canonical_ref = git::refs::branch(project.default_branch());
|
||||||
let mut set_canonical_refs: Vec<(git::Qualified, git::Oid)> = Vec::with_capacity(specs.len());
|
let mut set_canonical_refs: Vec<(git::Qualified, git::raw::ObjectType, git::Oid)> =
|
||||||
|
Vec::with_capacity(specs.len());
|
||||||
let working = git::raw::Repository::open(working)?;
|
let working = git::raw::Repository::open(working)?;
|
||||||
|
|
||||||
// For each refspec, push a ref or delete a ref.
|
// For each refspec, push a ref or delete a ref.
|
||||||
|
|
@ -348,7 +353,13 @@ pub fn run(
|
||||||
// Note that we *do* allow rolling back to a previous commit on the
|
// Note that we *do* allow rolling back to a previous commit on the
|
||||||
// canonical branch.
|
// canonical branch.
|
||||||
if let Some(canonical) = rules.canonical(dst.clone(), stored)? {
|
if let Some(canonical) = rules.canonical(dst.clone(), stored)? {
|
||||||
let canonical = canonical::Canonical::new(me, *src, canonical);
|
let kind = working
|
||||||
|
.find_object(**src, None)?
|
||||||
|
.kind()
|
||||||
|
.and_then(git::canonical::CanonicalObjectType::new)
|
||||||
|
.ok_or(Error::UnknownObjectType { oid: *src })?;
|
||||||
|
|
||||||
|
let canonical = canonical::Canonical::new(me, *src, kind, canonical);
|
||||||
match canonical.quorum(&working) {
|
match canonical.quorum(&working) {
|
||||||
Ok(quorum) => set_canonical_refs.push(quorum),
|
Ok(quorum) => set_canonical_refs.push(quorum),
|
||||||
Err(e) => canonical::io::handle_error(e, &dst, hints)?,
|
Err(e) => canonical::io::handle_error(e, &dst, hints)?,
|
||||||
|
|
@ -375,10 +386,10 @@ pub fn run(
|
||||||
if !ok.is_empty() {
|
if !ok.is_empty() {
|
||||||
let _ = stored.sign_refs(&signer)?;
|
let _ = stored.sign_refs(&signer)?;
|
||||||
|
|
||||||
for (refname, oid) in &set_canonical_refs {
|
for (refname, kind, oid) in &set_canonical_refs {
|
||||||
let print_update = || {
|
let print_update = || {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{} Canonical reference {} updated to {}",
|
"{} Canonical reference {} updated to target {kind} {}",
|
||||||
term::format::positive("✓"),
|
term::format::positive("✓"),
|
||||||
term::format::secondary(refname),
|
term::format::secondary(refname),
|
||||||
term::format::secondary(oid),
|
term::format::secondary(oid),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ use super::error;
|
||||||
|
|
||||||
pub(crate) struct Vote {
|
pub(crate) struct Vote {
|
||||||
did: Did,
|
did: Did,
|
||||||
commit: git::Oid,
|
oid: git::Oid,
|
||||||
|
kind: git::canonical::CanonicalObjectType,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates a vote to update a canonical reference during push.
|
/// Validates a vote to update a canonical reference during push.
|
||||||
|
|
@ -16,11 +17,17 @@ pub(crate) struct Canonical<'a, 'b> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> Canonical<'a, 'b> {
|
impl<'a, 'b> Canonical<'a, 'b> {
|
||||||
pub fn new(me: Did, head: git::Oid, canonical: git::canonical::Canonical<'a, 'b>) -> Self {
|
pub fn new(
|
||||||
|
me: Did,
|
||||||
|
head: git::Oid,
|
||||||
|
kind: git::canonical::CanonicalObjectType,
|
||||||
|
canonical: git::canonical::Canonical<'a, 'b>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
vote: Vote {
|
vote: Vote {
|
||||||
did: me,
|
did: me,
|
||||||
commit: head,
|
oid: head,
|
||||||
|
kind,
|
||||||
},
|
},
|
||||||
canonical,
|
canonical,
|
||||||
}
|
}
|
||||||
|
|
@ -46,31 +53,29 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
||||||
pub fn quorum(
|
pub fn quorum(
|
||||||
mut self,
|
mut self,
|
||||||
working: &Repository,
|
working: &Repository,
|
||||||
) -> Result<(git::Qualified<'a>, git::Oid), error::Canonical> {
|
) -> Result<(git::Qualified<'a>, git::raw::ObjectType, git::Oid), error::Canonical> {
|
||||||
let converges = self
|
let converges = self
|
||||||
.canonical
|
.canonical
|
||||||
.converges(working, (&self.vote.did, &self.vote.commit))?;
|
.converges(working, (&self.vote.did, &self.vote.oid))?;
|
||||||
if converges || self.canonical.has_no_tips() || self.canonical.is_only(&self.vote.did) {
|
if converges || self.canonical.has_no_tips() || self.canonical.is_only(&self.vote.did) {
|
||||||
self.canonical.modify_vote(self.vote.did, self.vote.commit);
|
self.canonical
|
||||||
|
.modify_vote(self.vote.did, self.vote.oid, self.vote.kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.canonical.quorum(working) {
|
match self.canonical.quorum(working) {
|
||||||
Ok((cref, quorum_head)) => {
|
Ok((cref, quorum_type, quorum_head)) => {
|
||||||
// Canonical head is an ancestor of head.
|
// Canonical head is an ancestor of head.
|
||||||
let is_ff = self.vote.commit == quorum_head
|
let is_ff = self.vote.oid == quorum_head
|
||||||
|| working
|
|| working
|
||||||
.graph_descendant_of(*self.vote.commit, *quorum_head)
|
.graph_descendant_of(*self.vote.oid, *quorum_head)
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
error::Canonical::graph_descendant(self.vote.commit, quorum_head, err)
|
error::Canonical::graph_descendant(self.vote.oid, quorum_head, err)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !is_ff && !converges {
|
if !is_ff && !converges {
|
||||||
Err(error::Canonical::heads_diverge(
|
Err(error::Canonical::heads_diverge(self.vote.oid, quorum_head))
|
||||||
self.vote.commit,
|
|
||||||
quorum_head,
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
Ok((cref, quorum_head))
|
Ok((cref, quorum_type, quorum_head))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
|
|
@ -80,7 +85,7 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
||||||
|
|
||||||
pub(crate) mod io {
|
pub(crate) mod io {
|
||||||
use radicle::git;
|
use radicle::git;
|
||||||
use radicle::git::canonical;
|
use radicle::git::canonical::error::QuorumError;
|
||||||
|
|
||||||
use crate::push::error;
|
use crate::push::error;
|
||||||
use crate::{hint, warn};
|
use crate::{hint, warn};
|
||||||
|
|
@ -110,19 +115,29 @@ pub(crate) mod io {
|
||||||
Err(e.into())
|
Err(e.into())
|
||||||
}
|
}
|
||||||
error::Canonical::Quorum(e) => match e {
|
error::Canonical::Quorum(e) => match e {
|
||||||
e @ canonical::QuorumError::Diverging { .. } => {
|
e @ QuorumError::DivergingCommits { .. } => {
|
||||||
warn(e.to_string());
|
warn(e.to_string());
|
||||||
warn("it is recommended to find a commit to agree upon");
|
warn("it is recommended to find a commit to agree upon");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
e @ canonical::QuorumError::NoCandidates { .. } => {
|
e @ QuorumError::DivergingTags { .. } => {
|
||||||
warn(e.to_string());
|
warn(e.to_string());
|
||||||
warn("it is recommended to find a commit to agree upon");
|
warn("it is recommended to find a tag to agree upon");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
canonical::QuorumError::Git(err) => {
|
e @ QuorumError::DifferentTypes { .. } => {
|
||||||
Err(error::CanonicalUnrecoverable::Git { source: err })
|
warn(e.to_string());
|
||||||
|
warn("it is recommended to find an object type (either commit or tag) to agree upon");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
e @ QuorumError::NoCandidates { .. } => {
|
||||||
|
warn(e.to_string());
|
||||||
|
warn(
|
||||||
|
"it is recommended to find an object (either commit or tag) to agree upon",
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
QuorumError::Git(err) => Err(error::CanonicalUnrecoverable::Git { source: err }),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ pub enum CanonicalUnrecoverable {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
GraphDescendant(#[from] GraphDescendant),
|
GraphDescendant(#[from] GraphDescendant),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Converges(#[from] canonical::ConvergesError),
|
Converges(#[from] canonical::error::ConvergesError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
HeadsDiverge(#[from] HeadsDiverge),
|
HeadsDiverge(#[from] HeadsDiverge),
|
||||||
#[error("failure while computing canonical reference: {source}")]
|
#[error("failure while computing canonical reference: {source}")]
|
||||||
|
|
@ -19,11 +19,11 @@ pub enum Canonical {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
GraphDescendant(GraphDescendant),
|
GraphDescendant(GraphDescendant),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Converges(#[from] canonical::ConvergesError),
|
Converges(#[from] canonical::error::ConvergesError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
HeadsDiverge(HeadsDiverge),
|
HeadsDiverge(HeadsDiverge),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Quorum(#[from] canonical::QuorumError),
|
Quorum(#[from] canonical::error::QuorumError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Canonical {
|
impl Canonical {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
|
pub mod error;
|
||||||
|
use error::*;
|
||||||
|
|
||||||
pub mod rules;
|
pub mod rules;
|
||||||
|
|
||||||
|
use nonempty::NonEmpty;
|
||||||
pub use rules::{MatchedRule, RawRule, Rules, ValidRule};
|
pub use rules::{MatchedRule, RawRule, Rules, ValidRule};
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::PathBuf;
|
use std::fmt;
|
||||||
|
|
||||||
|
use raw::ObjectType;
|
||||||
use raw::Repository;
|
use raw::Repository;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use crate::prelude::Did;
|
use crate::prelude::Did;
|
||||||
use crate::storage::ReadRepository;
|
use crate::storage::git;
|
||||||
|
|
||||||
use super::raw;
|
use super::raw;
|
||||||
use super::{Oid, Qualified};
|
use super::{Oid, Qualified};
|
||||||
|
|
@ -28,115 +33,70 @@ use super::{Oid, Qualified};
|
||||||
pub struct Canonical<'a, 'b> {
|
pub struct Canonical<'a, 'b> {
|
||||||
refname: Qualified<'a>,
|
refname: Qualified<'a>,
|
||||||
rule: &'b ValidRule,
|
rule: &'b ValidRule,
|
||||||
tips: BTreeMap<Did, Oid>,
|
tips: BTreeMap<Did, (Oid, CanonicalObjectType)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error that can occur when calculation the [`Canonical::quorum`].
|
/// Supported Git object types for canonical computation.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum QuorumError {
|
pub enum CanonicalObjectType {
|
||||||
/// Could not determine a quorum [`Oid`], due to diverging tips.
|
/// The Git object is a commit.
|
||||||
#[error("could not determine commit for canonical reference '{refname}', found diverging commits {longest} and {head}, with base commit {base} and threshold {threshold}")]
|
Commit,
|
||||||
Diverging {
|
/// The Git object is a tag.
|
||||||
refname: String,
|
Tag,
|
||||||
threshold: usize,
|
|
||||||
base: Oid,
|
|
||||||
longest: Oid,
|
|
||||||
head: Oid,
|
|
||||||
},
|
|
||||||
/// Could not determine a base candidate from the given set of delegates.
|
|
||||||
#[error("could not determine commit for canonical reference '{refname}', no commit with at least {threshold} vote(s) found (threshold not met)")]
|
|
||||||
NoCandidates { refname: String, threshold: usize },
|
|
||||||
/// An error occurred from [`git2`].
|
|
||||||
#[error(transparent)]
|
|
||||||
Git(#[from] git2::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
impl fmt::Display for CanonicalObjectType {
|
||||||
#[error("failed to check if {head} is an ancestor of {canonical} due to: {source}")]
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
pub struct GraphDescendant {
|
match self {
|
||||||
head: Oid,
|
CanonicalObjectType::Commit => f.write_str("commit"),
|
||||||
canonical: Oid,
|
CanonicalObjectType::Tag => f.write_str("tag"),
|
||||||
source: raw::Error,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
#[error("the commit {commit} for {did} is missing from the repository {repo:?}")]
|
|
||||||
pub struct MissingCommit {
|
|
||||||
repo: PathBuf,
|
|
||||||
did: Did,
|
|
||||||
commit: Oid,
|
|
||||||
source: raw::Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
#[error("could not determine whether the commit {commit} for {did} is part of the repository {repo:?} due to: {source}")]
|
|
||||||
pub struct InvalidCommit {
|
|
||||||
repo: PathBuf,
|
|
||||||
did: Did,
|
|
||||||
commit: Oid,
|
|
||||||
source: raw::Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum ConvergesError {
|
|
||||||
#[error(transparent)]
|
|
||||||
GraphDescendant(#[from] GraphDescendant),
|
|
||||||
#[error(transparent)]
|
|
||||||
MissingCommit(#[from] MissingCommit),
|
|
||||||
#[error(transparent)]
|
|
||||||
InvalidCommit(#[from] InvalidCommit),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConvergesError {
|
|
||||||
pub fn graph_descendant(head: Oid, canonical: Oid, source: raw::Error) -> Self {
|
|
||||||
Self::GraphDescendant(GraphDescendant {
|
|
||||||
head,
|
|
||||||
canonical,
|
|
||||||
source,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn missing_commit(repo: PathBuf, did: Did, commit: Oid, err: raw::Error) -> Self {
|
impl CanonicalObjectType {
|
||||||
Self::MissingCommit(MissingCommit {
|
/// Construct the [`CanonicalObjectType`] from a [`git2::ObjectType`].
|
||||||
repo,
|
pub fn new(kind: git::raw::ObjectType) -> Option<Self> {
|
||||||
did,
|
match kind {
|
||||||
commit,
|
ObjectType::Commit => Some(Self::Commit),
|
||||||
source: err,
|
ObjectType::Tag => Some(Self::Tag),
|
||||||
})
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn invalid_commit(repo: PathBuf, did: Did, commit: Oid, err: raw::Error) -> Self {
|
|
||||||
Self::InvalidCommit(InvalidCommit {
|
|
||||||
repo,
|
|
||||||
did,
|
|
||||||
commit,
|
|
||||||
source: err,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> Canonical<'a, 'b> {
|
impl<'a, 'b> Canonical<'a, 'b> {
|
||||||
/// Construct the set of canonical tips given for the given `rule` and
|
/// Construct the set of canonical tips given for the given `rule` and
|
||||||
/// the reference `refname`.
|
/// the reference `refname`.
|
||||||
pub fn new<S>(repo: &S, refname: Qualified<'a>, rule: &'b ValidRule) -> Result<Self, raw::Error>
|
pub fn new(
|
||||||
where
|
repo: &Repository,
|
||||||
S: ReadRepository,
|
refname: Qualified<'a>,
|
||||||
{
|
rule: &'b ValidRule,
|
||||||
|
) -> Result<Self, CanonicalError> {
|
||||||
let mut tips = BTreeMap::new();
|
let mut tips = BTreeMap::new();
|
||||||
for delegate in rule.allowed().iter() {
|
for delegate in rule.allowed().iter() {
|
||||||
match repo.reference_oid(delegate, &refname) {
|
let name = &refname.with_namespace(delegate.as_key().into());
|
||||||
Ok(tip) => {
|
|
||||||
tips.insert(*delegate, tip);
|
let reference = match repo.find_reference(name) {
|
||||||
}
|
Ok(reference) => reference,
|
||||||
Err(e) if super::ext::is_not_found_err(&e) => {
|
Err(e) if super::ext::is_not_found_err(&e) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
target: "radicle",
|
target: "radicle",
|
||||||
"Missing `refs/namespaces/{}/{refname}` while calculating the canonical reference",
|
"Missing `{name}` while calculating the canonical reference",
|
||||||
delegate.as_key()
|
|
||||||
);
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(CanonicalError::find_reference(name, e)),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
let Some(oid) = reference.target() else {
|
||||||
|
log::warn!(target: "radicle", "Missing target for reference `{name}`");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let kind = Self::find_object_for(delegate, oid.into(), repo)?;
|
||||||
|
|
||||||
|
tips.insert(*delegate, (oid.into(), kind));
|
||||||
}
|
}
|
||||||
Ok(Canonical {
|
Ok(Canonical {
|
||||||
refname,
|
refname,
|
||||||
|
|
@ -145,9 +105,31 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the set of [`Did`]s and their [`Oid`] tip.
|
pub fn find_object_for(
|
||||||
pub fn tips(&self) -> impl Iterator<Item = (&Did, &Oid)> {
|
did: &Did,
|
||||||
self.tips.iter()
|
oid: Oid,
|
||||||
|
repo: &raw::Repository,
|
||||||
|
) -> Result<CanonicalObjectType, CanonicalError> {
|
||||||
|
match repo.find_object(*oid, None) {
|
||||||
|
Ok(object) => object
|
||||||
|
.kind()
|
||||||
|
.and_then(CanonicalObjectType::new)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CanonicalError::invalid_object_type(
|
||||||
|
repo.path().to_path_buf(),
|
||||||
|
*did,
|
||||||
|
oid,
|
||||||
|
object.kind(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Err(err) if super::ext::is_not_found_err(&err) => Err(CanonicalError::missing_object(
|
||||||
|
repo.path().to_path_buf(),
|
||||||
|
*did,
|
||||||
|
oid,
|
||||||
|
err,
|
||||||
|
)),
|
||||||
|
Err(err) => Err(CanonicalError::find_object(oid, err)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if there were no tips found for any of the DIDs for
|
/// Returns `true` if there were no tips found for any of the DIDs for
|
||||||
|
|
@ -165,8 +147,8 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
||||||
/// In some cases, we allow the vote to be modified. For example, when the
|
/// In some cases, we allow the vote to be modified. For example, when the
|
||||||
/// `did` is pushing a new commit, we may want to see if the new commit will
|
/// `did` is pushing a new commit, we may want to see if the new commit will
|
||||||
/// reach a quorum.
|
/// reach a quorum.
|
||||||
pub fn modify_vote(&mut self, did: Did, new: Oid) {
|
pub fn modify_vote(&mut self, did: Did, oid: Oid, kind: CanonicalObjectType) {
|
||||||
self.tips.insert(did, new);
|
self.tips.insert(did, (oid, kind));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check that the provided `did` is part of the set of allowed
|
/// Check that the provided `did` is part of the set of allowed
|
||||||
|
|
@ -191,28 +173,116 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
||||||
repo: &Repository,
|
repo: &Repository,
|
||||||
(candidate, commit): (&Did, &Oid),
|
(candidate, commit): (&Did, &Oid),
|
||||||
) -> Result<bool, ConvergesError> {
|
) -> Result<bool, ConvergesError> {
|
||||||
|
/// Ensures [`Oid`]s are of the same object type
|
||||||
|
enum Objects {
|
||||||
|
Commits(NonEmpty<Oid>),
|
||||||
|
Tags(NonEmpty<Oid>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Objects {
|
||||||
|
fn new(oid: Oid, kind: CanonicalObjectType) -> Self {
|
||||||
|
match kind {
|
||||||
|
CanonicalObjectType::Commit => Self::Commits(NonEmpty::new(oid)),
|
||||||
|
CanonicalObjectType::Tag => Self::Tags(NonEmpty::new(oid)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert(
|
||||||
|
mut self,
|
||||||
|
oid: Oid,
|
||||||
|
kind: CanonicalObjectType,
|
||||||
|
) -> Result<Self, CanonicalObjectType> {
|
||||||
|
match self {
|
||||||
|
Objects::Commits(ref mut commits) => match kind {
|
||||||
|
CanonicalObjectType::Commit => {
|
||||||
|
commits.push(oid);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
CanonicalObjectType::Tag => Err(CanonicalObjectType::Tag),
|
||||||
|
},
|
||||||
|
Objects::Tags(ref mut tags) => match kind {
|
||||||
|
CanonicalObjectType::Commit => {
|
||||||
|
tags.push(oid);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
CanonicalObjectType::Tag => Err(CanonicalObjectType::Commit),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let heads = {
|
let heads = {
|
||||||
let mut heads = self
|
let heads = self
|
||||||
.tips
|
.tips
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(did, tip)| (did != candidate).then_some((did, tip)));
|
.filter_map(|(did, tip)| (did != candidate).then_some((did, tip)));
|
||||||
heads.try_fold(
|
|
||||||
Vec::with_capacity(heads.size_hint().0),
|
let mut objects = None;
|
||||||
|mut heads, (did, head)| {
|
|
||||||
heads.push(Self::ensure_commit(*did, *head, repo)?);
|
for (did, (oid, _)) in heads {
|
||||||
Ok::<_, ConvergesError>(heads)
|
let kind = find_object_for(did, *oid, repo)?;
|
||||||
},
|
let oid = *oid;
|
||||||
)?
|
match objects {
|
||||||
|
None => objects = Some(Objects::new(oid, kind)),
|
||||||
|
Some(objs) => {
|
||||||
|
objects = Some(objs.insert(oid, kind).map_err(|expected| {
|
||||||
|
ConvergesError::mismatched_object(
|
||||||
|
repo.path().to_path_buf(),
|
||||||
|
oid,
|
||||||
|
kind,
|
||||||
|
expected,
|
||||||
|
)
|
||||||
|
})?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objects
|
||||||
};
|
};
|
||||||
for head in heads {
|
|
||||||
let (ahead, behind) = repo
|
match heads {
|
||||||
.graph_ahead_behind(**commit, *head)
|
None => Ok(true),
|
||||||
.map_err(|err| ConvergesError::graph_descendant(*commit, head, err))?;
|
Some(Objects::Tags(_)) => Ok(true),
|
||||||
if ahead * behind == 0 {
|
Some(Objects::Commits(heads)) => {
|
||||||
return Ok(true);
|
for head in heads {
|
||||||
|
let (ahead, behind) = repo
|
||||||
|
.graph_ahead_behind(**commit, *head)
|
||||||
|
.map_err(|err| ConvergesError::graph_descendant(*commit, head, err))?;
|
||||||
|
if ahead * behind == 0 {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(false)
|
}
|
||||||
|
|
||||||
|
fn quorum_tag(&self) -> Result<Oid, QuorumError> {
|
||||||
|
let voting =
|
||||||
|
TagVoting::from_targets(self.tips.values().filter_map(|(commit, kind)| {
|
||||||
|
(*kind == CanonicalObjectType::Tag).then_some(*commit)
|
||||||
|
}));
|
||||||
|
let mut votes = voting.votes();
|
||||||
|
|
||||||
|
// Keep tags which pass the threshold.
|
||||||
|
votes.votes_past_threshold(self.threshold());
|
||||||
|
|
||||||
|
if votes.number_of_candidates() > 1 {
|
||||||
|
return Err(QuorumError::DivergingTags {
|
||||||
|
refname: self.refname.to_string(),
|
||||||
|
threshold: self.threshold(),
|
||||||
|
candidates: votes.candidates().cloned().collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tag = votes
|
||||||
|
.pop_first_candidate()
|
||||||
|
.ok_or(QuorumError::NoCandidates {
|
||||||
|
refname: self.refname.to_string(),
|
||||||
|
threshold: self.threshold(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((*tag).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Computes the quorum or "canonical" tip based on the tips, of `Canonical`,
|
/// Computes the quorum or "canonical" tip based on the tips, of `Canonical`,
|
||||||
|
|
@ -222,41 +292,36 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
||||||
///
|
///
|
||||||
/// Also returns an error if `heads` is empty or `threshold` cannot be
|
/// Also returns an error if `heads` is empty or `threshold` cannot be
|
||||||
/// satisified with the number of heads given.
|
/// satisified with the number of heads given.
|
||||||
pub fn quorum(self, repo: &raw::Repository) -> Result<(Qualified<'a>, Oid), QuorumError> {
|
fn quorum_commit(&self, repo: &raw::Repository) -> Result<Oid, QuorumError> {
|
||||||
let mut candidates = BTreeMap::<_, usize>::new();
|
let mut voting =
|
||||||
|
CommitVoting::from_targets(self.tips.values().filter_map(|(commit, kind)| {
|
||||||
// Build a list of candidate commits and count how many "votes" each of them has.
|
(*kind == CanonicalObjectType::Commit).then_some(*commit)
|
||||||
// Commits get a point for each direct vote, as well as for being part of the ancestry
|
}));
|
||||||
// of a commit given to this function. Only commits given to the function are considered.
|
while let Some(targets) = voting.next_candidate() {
|
||||||
for (i, head) in self.tips.values().enumerate() {
|
for (candidate, other) in targets {
|
||||||
// Add a direct vote for this head.
|
let base = Oid::from(repo.merge_base(*candidate, *other)?);
|
||||||
*candidates.entry(*head).or_default() += 1;
|
voting.found_merge_base(MergeBase {
|
||||||
|
candidate,
|
||||||
// Compare this head to all other heads ahead of it in the list.
|
other,
|
||||||
for other in self.tips.values().skip(i + 1) {
|
base,
|
||||||
// N.b. if heads are equal then skip it, otherwise it will end up as
|
});
|
||||||
// a double vote.
|
|
||||||
if *head == *other {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let base = Oid::from(repo.merge_base(**head, **other)?);
|
|
||||||
|
|
||||||
if base == *other || base == *head {
|
|
||||||
*candidates.entry(base).or_default() += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Keep commits which pass the threshold.
|
let mut votes = voting.votes();
|
||||||
candidates.retain(|_, votes| *votes >= self.threshold());
|
|
||||||
|
|
||||||
let (mut longest, _) = candidates.pop_first().ok_or(QuorumError::NoCandidates {
|
// Keep commits which pass the threshold.
|
||||||
refname: self.refname.to_string(),
|
votes.votes_past_threshold(self.threshold());
|
||||||
threshold: self.threshold(),
|
|
||||||
})?;
|
let mut longest = votes
|
||||||
|
.pop_first_candidate()
|
||||||
|
.ok_or(QuorumError::NoCandidates {
|
||||||
|
refname: self.refname.to_string(),
|
||||||
|
threshold: self.threshold(),
|
||||||
|
})?;
|
||||||
|
|
||||||
// Now that all scores are calculated, figure out what is the longest branch
|
// Now that all scores are calculated, figure out what is the longest branch
|
||||||
// that passes the threshold. In case of divergence, return an error.
|
// that passes the threshold. In case of divergence, return an error.
|
||||||
for head in candidates.keys() {
|
for head in votes.candidates() {
|
||||||
let base = repo.merge_base(**head, *longest)?;
|
let base = repo.merge_base(**head, *longest)?;
|
||||||
|
|
||||||
if base == *longest {
|
if base == *longest {
|
||||||
|
|
@ -286,7 +351,7 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
||||||
// o (base)
|
// o (base)
|
||||||
// |
|
// |
|
||||||
//
|
//
|
||||||
return Err(QuorumError::Diverging {
|
return Err(QuorumError::DivergingCommits {
|
||||||
refname: self.refname.to_string(),
|
refname: self.refname.to_string(),
|
||||||
threshold: self.threshold(),
|
threshold: self.threshold(),
|
||||||
base: base.into(),
|
base: base.into(),
|
||||||
|
|
@ -295,27 +360,206 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok((self.refname, (*longest).into()))
|
|
||||||
|
Ok((*longest).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the quorum or "canonical" tip based on the tips, of `Canonical`,
|
||||||
|
/// and the threshold. This can be described as the latest commit that is
|
||||||
|
/// included in at least `threshold` histories. In case there are multiple tips
|
||||||
|
/// passing the threshold, and they are divergent, an error is returned.
|
||||||
|
///
|
||||||
|
/// Also returns an error if `heads` is empty or `threshold` cannot be
|
||||||
|
/// satisified with the number of heads given.
|
||||||
|
pub fn quorum(
|
||||||
|
self,
|
||||||
|
repo: &raw::Repository,
|
||||||
|
) -> Result<(Qualified<'a>, ObjectType, Oid), QuorumError> {
|
||||||
|
let (oid, kind) = match (self.quorum_commit(repo), self.quorum_tag()) {
|
||||||
|
(Ok(commit), Err(_)) => Ok((commit, ObjectType::Commit)),
|
||||||
|
(Err(_), Ok(tag)) => Ok((tag, ObjectType::Tag)),
|
||||||
|
(Ok(_), Ok(_)) => Err(QuorumError::DifferentTypes {
|
||||||
|
refname: self.refname.clone().to_string(),
|
||||||
|
}),
|
||||||
|
(Err(commit), Err(QuorumError::NoCandidates { .. })) => Err(commit),
|
||||||
|
(Err(QuorumError::NoCandidates { .. }), Err(tag)) => Err(tag),
|
||||||
|
(Err(err), _) => Err(err),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok((self.refname, kind, oid))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn threshold(&self) -> usize {
|
fn threshold(&self) -> usize {
|
||||||
(*self.rule.threshold()).into()
|
(*self.rule.threshold()).into()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_commit(from: Did, commit: Oid, working: &Repository) -> Result<Oid, ConvergesError> {
|
/// Keep track of [`Votes`] for quorums involving tag objects.
|
||||||
match working.find_commit(*commit).map(|_| commit) {
|
struct TagVoting {
|
||||||
Ok(oid) => Ok(oid),
|
votes: Votes,
|
||||||
Err(err) if err.code() == raw::ErrorCode::NotFound => Err(
|
}
|
||||||
ConvergesError::missing_commit(working.path().to_path_buf(), from, commit, err),
|
|
||||||
),
|
impl TagVoting {
|
||||||
Err(err) => Err(ConvergesError::invalid_commit(
|
fn from_targets(targets: impl Iterator<Item = Oid>) -> Self {
|
||||||
working.path().to_path_buf(),
|
let votes = targets.fold(Votes::default(), |mut votes, oid| {
|
||||||
from,
|
votes.vote(oid);
|
||||||
commit,
|
votes
|
||||||
err,
|
});
|
||||||
)),
|
Self { votes }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn votes(self) -> Votes {
|
||||||
|
self.votes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep track of [`Votes`] for quorums involving commit objects.
|
||||||
|
///
|
||||||
|
/// Build a list of candidate commits and count how many "votes" each of them
|
||||||
|
/// has. Commits get a point for each direct vote, as well as for being part of
|
||||||
|
/// the ancestry of a commit given to this function.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CommitVoting {
|
||||||
|
candidates: Vec<(Oid, Vec<Oid>)>,
|
||||||
|
votes: Votes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommitVoting {
|
||||||
|
/// Build the initial set voting.
|
||||||
|
fn from_targets(targets: impl Iterator<Item = Oid> + Clone) -> Self {
|
||||||
|
let ts = targets.clone();
|
||||||
|
let (candidates, votes) = targets.enumerate().fold(
|
||||||
|
(Vec::new(), Votes::default()),
|
||||||
|
|(mut candidates, mut votes), (i, oid)| {
|
||||||
|
candidates.push((oid, ts.clone().skip(i + 1).collect()));
|
||||||
|
votes.vote(oid);
|
||||||
|
(candidates, votes)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Self { candidates, votes }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the next candidate to be considered for ancestry votes.
|
||||||
|
///
|
||||||
|
/// The first of each pair will be the candidate commit, which should be
|
||||||
|
/// compared to the other commit to see what their common merge base is. The
|
||||||
|
/// merge base is then recorded using [`MergeBase`] and is recorded using
|
||||||
|
/// [`CommitVoting::found_merge_base`].
|
||||||
|
fn next_candidate(&mut self) -> Option<impl Iterator<Item = (Oid, Oid)>> {
|
||||||
|
self.candidates
|
||||||
|
.pop()
|
||||||
|
.map(|(oid, others)| others.into_iter().map(move |other| (oid, other)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record a merge base, and add to the vote if necessary.
|
||||||
|
fn found_merge_base(
|
||||||
|
&mut self,
|
||||||
|
MergeBase {
|
||||||
|
candidate,
|
||||||
|
other,
|
||||||
|
base,
|
||||||
|
}: MergeBase,
|
||||||
|
) {
|
||||||
|
// Avoid double counting the same commits
|
||||||
|
let is_same = candidate == other;
|
||||||
|
if !is_same && (base == candidate || base == other) {
|
||||||
|
self.votes.vote(base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finish the voting process and get the [`Votes`] from the
|
||||||
|
/// [`CommitVoting`].
|
||||||
|
fn votes(self) -> Votes {
|
||||||
|
self.votes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record a merge base between `candidate` and `other`.
|
||||||
|
struct MergeBase {
|
||||||
|
/// The candidate commit for the merge base.
|
||||||
|
candidate: Oid,
|
||||||
|
/// The commit that is being compared against for the merge base.
|
||||||
|
other: Oid,
|
||||||
|
/// The computed merge base commit.
|
||||||
|
base: Oid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count the number of votes per [`Oid`].
|
||||||
|
///
|
||||||
|
/// Note that the count cannot exceed 255, since that is the maximum number the
|
||||||
|
/// `threshold` value can be.
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
struct Votes {
|
||||||
|
inner: BTreeMap<Oid, u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Votes {
|
||||||
|
/// Increase the vote count for `oid`.
|
||||||
|
///
|
||||||
|
/// If `oid` does not exist in the set of [`Votes`] yet, then no vote will
|
||||||
|
/// be added.
|
||||||
|
#[inline]
|
||||||
|
fn vote(&mut self, oid: Oid) {
|
||||||
|
self.safe_inc(oid, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter the candidates by the ones that have a number of votes that pass
|
||||||
|
/// the `threshold`.
|
||||||
|
#[inline]
|
||||||
|
fn votes_past_threshold(&mut self, threshold: usize) {
|
||||||
|
self.inner.retain(|_, votes| *votes as usize >= threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the number of candidates this set of votes has.
|
||||||
|
#[inline]
|
||||||
|
fn number_of_candidates(&self) -> usize {
|
||||||
|
self.inner.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the set candidates.
|
||||||
|
#[inline]
|
||||||
|
fn candidates(&self) -> impl Iterator<Item = &Oid> {
|
||||||
|
self.inner.keys()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop off the first candidate from the set of votes.
|
||||||
|
#[inline]
|
||||||
|
fn pop_first_candidate(&mut self) -> Option<Oid> {
|
||||||
|
self.inner.pop_first().map(|(oid, _)| oid)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn safe_inc(&mut self, oid: Oid, n: u8) {
|
||||||
|
let votes = self.inner.entry(oid).or_default();
|
||||||
|
*votes = votes.saturating_add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_object_for(
|
||||||
|
did: &Did,
|
||||||
|
oid: Oid,
|
||||||
|
repo: &raw::Repository,
|
||||||
|
) -> Result<CanonicalObjectType, FindObjectError> {
|
||||||
|
match repo.find_object(*oid, None) {
|
||||||
|
Ok(object) => object
|
||||||
|
.kind()
|
||||||
|
.and_then(CanonicalObjectType::new)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
FindObjectError::invalid_object_type(
|
||||||
|
repo.path().to_path_buf(),
|
||||||
|
*did,
|
||||||
|
oid,
|
||||||
|
object.kind(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Err(err) if super::ext::is_not_found_err(&err) => Err(FindObjectError::missing_object(
|
||||||
|
repo.path().to_path_buf(),
|
||||||
|
*did,
|
||||||
|
oid,
|
||||||
|
err,
|
||||||
|
)),
|
||||||
|
Err(err) => Err(FindObjectError::find_object(oid, err)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -334,13 +578,19 @@ mod tests {
|
||||||
threshold: usize,
|
threshold: usize,
|
||||||
repo: &git::raw::Repository,
|
repo: &git::raw::Repository,
|
||||||
) -> Result<Oid, QuorumError> {
|
) -> Result<Oid, QuorumError> {
|
||||||
let tips: BTreeMap<Did, Oid> = heads
|
let tips: BTreeMap<Did, (Oid, CanonicalObjectType)> = heads
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, head)| {
|
.map(|(i, head)| {
|
||||||
let signer = Device::mock_from_seed([(i + 1) as u8; 32]);
|
let signer = Device::mock_from_seed([(i + 1) as u8; 32]);
|
||||||
let did = Did::from(signer.public_key());
|
let did = Did::from(signer.public_key());
|
||||||
(did, (*head).into())
|
let kind = repo
|
||||||
|
.find_object(*head, None)
|
||||||
|
.unwrap()
|
||||||
|
.kind()
|
||||||
|
.and_then(CanonicalObjectType::new)
|
||||||
|
.unwrap();
|
||||||
|
(did, ((*head).into(), kind))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -360,7 +610,7 @@ mod tests {
|
||||||
rule: &rule,
|
rule: &rule,
|
||||||
}
|
}
|
||||||
.quorum(repo)
|
.quorum(repo)
|
||||||
.map(|(_, oid)| oid)
|
.map(|(_, _, oid)| oid)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -397,6 +647,62 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quorum_groups() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let (repo, c0) = fixtures::repository(tmp.path());
|
||||||
|
let c0: git::Oid = c0.into();
|
||||||
|
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
||||||
|
let c2 = fixtures::commit("C2", &[*c0], &repo);
|
||||||
|
|
||||||
|
eprintln!("C0: {c0}");
|
||||||
|
eprintln!("C1: {c1}");
|
||||||
|
eprintln!("C2: {c2}");
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*c1, *c2, *c1, *c2], 2, &repo),
|
||||||
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*c1, *c2], 1, &repo),
|
||||||
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quorum_tag() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let (repo, c0) = fixtures::repository(tmp.path());
|
||||||
|
let c0: git::Oid = c0.into();
|
||||||
|
let c1 = fixtures::commit("C1", &[*c0], &repo);
|
||||||
|
let t1 = fixtures::tag("v1", "T1", *c1, &repo);
|
||||||
|
let t2 = fixtures::tag("v2", "T2", *c1, &repo);
|
||||||
|
|
||||||
|
eprintln!("C0: {c0}");
|
||||||
|
eprintln!("C1: {c1}");
|
||||||
|
eprintln!("T1: {t1}");
|
||||||
|
eprintln!("T2: {t2}");
|
||||||
|
|
||||||
|
assert_eq!(quorum(&[*t1], 1, &repo).unwrap(), t1);
|
||||||
|
assert_eq!(quorum(&[*t1, *t1], 2, &repo).unwrap(), t1);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*t1, *t2], 2, &repo),
|
||||||
|
Err(QuorumError::NoCandidates { .. })
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*t1, *c1], 1, &repo),
|
||||||
|
Err(QuorumError::DifferentTypes { .. })
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
quorum(&[*t1, *t2], 1, &repo),
|
||||||
|
Err(QuorumError::DivergingTags { .. })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_quorum() {
|
fn test_quorum() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|
@ -447,15 +753,15 @@ mod tests {
|
||||||
// C0
|
// C0
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c1, *c2, *b2], 1, &repo),
|
quorum(&[*c1, *c2, *b2], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c2, *b2], 1, &repo),
|
quorum(&[*c2, *b2], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*b2, *c2], 1, &repo),
|
quorum(&[*b2, *c2], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c2, *b2], 2, &repo),
|
quorum(&[*c2, *b2], 2, &repo),
|
||||||
|
|
@ -471,7 +777,7 @@ mod tests {
|
||||||
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
|
quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
|
|
||||||
// B2 C2 C3
|
// B2 C2 C3
|
||||||
|
|
@ -500,7 +806,7 @@ mod tests {
|
||||||
// C0
|
// C0
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c2, *b2, *a1], 1, &repo),
|
quorum(&[*c2, *b2, *a1], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*c2, *b2, *a1], 2, &repo),
|
quorum(&[*c2, *b2, *a1], 2, &repo),
|
||||||
|
|
@ -520,23 +826,23 @@ mod tests {
|
||||||
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 4, &repo).unwrap(), c0,);
|
assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 4, &repo).unwrap(), c0,);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo),
|
quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
|
quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*a1, *a1, *c2], 1, &repo),
|
quorum(&[*a1, *a1, *c2], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
|
quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
|
quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
|
|
||||||
// M2 M1
|
// M2 M1
|
||||||
|
|
@ -549,11 +855,11 @@ mod tests {
|
||||||
assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
|
assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m2], 1, &repo),
|
quorum(&[*m1, *m2], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m2, *m1], 1, &repo),
|
quorum(&[*m2, *m1], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m2], 2, &repo),
|
quorum(&[*m1, *m2], 2, &repo),
|
||||||
|
|
@ -561,11 +867,11 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m2, *c2], 1, &repo),
|
quorum(&[*m1, *m2, *c2], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *a1], 1, &repo),
|
quorum(&[*m1, *a1], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *a1], 2, &repo),
|
quorum(&[*m1, *a1], 2, &repo),
|
||||||
|
|
@ -608,7 +914,7 @@ mod tests {
|
||||||
// C0
|
// C0
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m2], 1, &repo),
|
quorum(&[*m1, *m2], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m2], 2, &repo),
|
quorum(&[*m1, *m2], 2, &repo),
|
||||||
|
|
@ -624,7 +930,7 @@ mod tests {
|
||||||
// C0
|
// C0
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m3], 1, &repo),
|
quorum(&[*m1, *m3], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m1, *m3], 2, &repo),
|
quorum(&[*m1, *m3], 2, &repo),
|
||||||
|
|
@ -632,7 +938,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m3, *m1], 1, &repo),
|
quorum(&[*m3, *m1], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m3, *m1], 2, &repo),
|
quorum(&[*m3, *m1], 2, &repo),
|
||||||
|
|
@ -640,7 +946,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m3, *m2], 1, &repo),
|
quorum(&[*m3, *m2], 1, &repo),
|
||||||
Err(QuorumError::Diverging { .. })
|
Err(QuorumError::DivergingCommits { .. })
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
quorum(&[*m3, *m2], 2, &repo),
|
quorum(&[*m3, *m2], 2, &repo),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::{git::raw, git::Oid, prelude::Did};
|
||||||
|
|
||||||
|
use super::CanonicalObjectType;
|
||||||
|
|
||||||
|
/// Error that can occur when calculation the [`Canonical::quorum`].
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum QuorumError {
|
||||||
|
/// Could not determine a quorum [`Oid`], due to diverging tips.
|
||||||
|
#[error("could not determine target commit for canonical reference '{refname}', found diverging commits {longest} and {head}, with base commit {base} and threshold {threshold}")]
|
||||||
|
DivergingCommits {
|
||||||
|
refname: String,
|
||||||
|
threshold: usize,
|
||||||
|
base: Oid,
|
||||||
|
longest: Oid,
|
||||||
|
head: Oid,
|
||||||
|
},
|
||||||
|
#[error("could not determine target tag for canonical reference '{refname}', found multiple candidates with threshold {threshold}")]
|
||||||
|
DivergingTags {
|
||||||
|
refname: String,
|
||||||
|
threshold: usize,
|
||||||
|
candidates: Vec<Oid>,
|
||||||
|
},
|
||||||
|
#[error("could not determine target for canonical reference '{refname}', found objects of different types")]
|
||||||
|
DifferentTypes { refname: String },
|
||||||
|
/// Could not determine a base candidate from the given set of delegates.
|
||||||
|
#[error("could not determine target for canonical reference '{refname}', no object with at least {threshold} vote(s) found (threshold not met)")]
|
||||||
|
NoCandidates { refname: String, threshold: usize },
|
||||||
|
/// An error occurred from [`git2`].
|
||||||
|
#[error(transparent)]
|
||||||
|
Git(#[from] git2::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("failed to check if {head} is an ancestor of {canonical} due to: {source}")]
|
||||||
|
pub struct GraphDescendant {
|
||||||
|
head: Oid,
|
||||||
|
canonical: Oid,
|
||||||
|
source: raw::Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("the commit {commit} for {did} is missing from the repository {repo:?}")]
|
||||||
|
pub struct MissingObject {
|
||||||
|
repo: PathBuf,
|
||||||
|
did: Did,
|
||||||
|
commit: Oid,
|
||||||
|
source: raw::Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("could not determine whether the commit {commit} for {did} is part of the repository {repo:?} due to: {source}")]
|
||||||
|
pub struct InvalidObject {
|
||||||
|
repo: PathBuf,
|
||||||
|
did: Did,
|
||||||
|
commit: Oid,
|
||||||
|
source: raw::Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("the object {oid} for {did} in the repository {repo:?} is of unexpected type {kind:?}")]
|
||||||
|
pub struct InvalidObjectType {
|
||||||
|
repo: PathBuf,
|
||||||
|
did: Did,
|
||||||
|
oid: Oid,
|
||||||
|
kind: Option<git2::ObjectType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("the object {oid} in the repository {repo:?} is of unexpected type {found} and was expected to be {expected}")]
|
||||||
|
pub struct MismatchedObject {
|
||||||
|
repo: PathBuf,
|
||||||
|
oid: Oid,
|
||||||
|
found: CanonicalObjectType,
|
||||||
|
expected: CanonicalObjectType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CanonicalError {
|
||||||
|
#[error(transparent)]
|
||||||
|
InvalidObjectType(#[from] InvalidObjectType),
|
||||||
|
#[error(transparent)]
|
||||||
|
MissingObject(#[from] MissingObject),
|
||||||
|
#[error("failed to find object {oid} due to: {source}")]
|
||||||
|
FindObject { oid: Oid, source: git2::Error },
|
||||||
|
#[error("failed to find reference {name} due to: {source}")]
|
||||||
|
FindReference { name: String, source: git2::Error },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CanonicalError {
|
||||||
|
pub(super) fn invalid_object_type(
|
||||||
|
repo: PathBuf,
|
||||||
|
did: Did,
|
||||||
|
oid: Oid,
|
||||||
|
kind: Option<git2::ObjectType>,
|
||||||
|
) -> Self {
|
||||||
|
InvalidObjectType {
|
||||||
|
repo,
|
||||||
|
did,
|
||||||
|
oid,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn missing_object(repo: PathBuf, did: Did, oid: Oid, err: git2::Error) -> Self {
|
||||||
|
MissingObject {
|
||||||
|
repo,
|
||||||
|
did,
|
||||||
|
commit: oid,
|
||||||
|
source: err,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn find_object(oid: Oid, err: git2::Error) -> Self {
|
||||||
|
Self::FindObject { oid, source: err }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn find_reference(name: &str, e: git2::Error) -> CanonicalError {
|
||||||
|
Self::FindReference {
|
||||||
|
name: name.to_string(),
|
||||||
|
source: e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum FindObjectError {
|
||||||
|
#[error(transparent)]
|
||||||
|
InvalidObjectType(#[from] InvalidObjectType),
|
||||||
|
#[error(transparent)]
|
||||||
|
MissingObject(#[from] MissingObject),
|
||||||
|
#[error("failed to find object {oid} due to: {source}")]
|
||||||
|
FindObject { oid: Oid, source: git2::Error },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FindObjectError {
|
||||||
|
pub(super) fn find_object(oid: Oid, err: git2::Error) -> Self {
|
||||||
|
Self::FindObject { oid, source: err }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn missing_object(repo: PathBuf, did: Did, oid: Oid, err: git2::Error) -> Self {
|
||||||
|
MissingObject {
|
||||||
|
repo,
|
||||||
|
did,
|
||||||
|
commit: oid,
|
||||||
|
source: err,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn invalid_object_type(
|
||||||
|
repo: PathBuf,
|
||||||
|
did: Did,
|
||||||
|
oid: Oid,
|
||||||
|
kind: Option<git2::ObjectType>,
|
||||||
|
) -> Self {
|
||||||
|
InvalidObjectType {
|
||||||
|
repo,
|
||||||
|
did,
|
||||||
|
oid,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ConvergesError {
|
||||||
|
#[error(transparent)]
|
||||||
|
GraphDescendant(#[from] GraphDescendant),
|
||||||
|
#[error(transparent)]
|
||||||
|
MismatchedObject(#[from] MismatchedObject),
|
||||||
|
#[error(transparent)]
|
||||||
|
FindObjectError(#[from] FindObjectError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConvergesError {
|
||||||
|
pub(super) fn graph_descendant(head: Oid, canonical: Oid, source: raw::Error) -> Self {
|
||||||
|
Self::GraphDescendant(GraphDescendant {
|
||||||
|
head,
|
||||||
|
canonical,
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mismatched_object(
|
||||||
|
repo: PathBuf,
|
||||||
|
oid: Oid,
|
||||||
|
found: CanonicalObjectType,
|
||||||
|
expected: CanonicalObjectType,
|
||||||
|
) -> Self {
|
||||||
|
Self::MismatchedObject(MismatchedObject {
|
||||||
|
repo,
|
||||||
|
oid,
|
||||||
|
found,
|
||||||
|
expected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ use serde_json as json;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::git;
|
use crate::git;
|
||||||
|
use crate::git::canonical;
|
||||||
use crate::git::canonical::Canonical;
|
use crate::git::canonical::Canonical;
|
||||||
use crate::git::fmt::{refname, RefString};
|
use crate::git::fmt::{refname, RefString};
|
||||||
use crate::git::refspec::QualifiedPattern;
|
use crate::git::refspec::QualifiedPattern;
|
||||||
|
|
@ -634,9 +635,9 @@ impl Rules {
|
||||||
&'a self,
|
&'a self,
|
||||||
refname: Qualified<'b>,
|
refname: Qualified<'b>,
|
||||||
repo: &Repository,
|
repo: &Repository,
|
||||||
) -> Result<Option<Canonical<'b, 'a>>, git::raw::Error> {
|
) -> Result<Option<Canonical<'b, 'a>>, canonical::error::CanonicalError> {
|
||||||
if let Some((_, rule)) = self.matches(&refname).next() {
|
if let Some((_, rule)) = self.matches(&refname).next() {
|
||||||
Ok(Some(Canonical::new(repo, refname, rule)?))
|
Ok(Some(Canonical::new(&repo.backend, refname, rule)?))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
@ -1206,7 +1207,7 @@ mod tests {
|
||||||
canonical
|
canonical
|
||||||
.quorum(&repo)
|
.quorum(&repo)
|
||||||
.unwrap_or_else(|e| panic!("quorum error for {refname}: {e}")),
|
.unwrap_or_else(|e| panic!("quorum error for {refname}: {e}")),
|
||||||
(refname, oid),
|
(refname, git::raw::ObjectType::Tag, oid),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ pub enum RepositoryError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
GitExt(#[from] git_ext::Error),
|
GitExt(#[from] git_ext::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Quorum(#[from] canonical::QuorumError),
|
Quorum(#[from] canonical::error::QuorumError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Refs(#[from] refs::Error),
|
Refs(#[from] refs::Error),
|
||||||
#[error("missing canonical reference rule for default branch")]
|
#[error("missing canonical reference rule for default branch")]
|
||||||
|
|
@ -126,6 +126,8 @@ pub enum RepositoryError {
|
||||||
DefaultBranchRule(#[from] doc::DefaultBranchRuleError),
|
DefaultBranchRule(#[from] doc::DefaultBranchRuleError),
|
||||||
#[error("failed to get canonical reference rules: {0}")]
|
#[error("failed to get canonical reference rules: {0}")]
|
||||||
CanonicalRefs(#[from] doc::CanonicalRefsError),
|
CanonicalRefs(#[from] doc::CanonicalRefsError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Canonical(#[from] canonical::error::CanonicalError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepositoryError {
|
impl RepositoryError {
|
||||||
|
|
|
||||||
|
|
@ -720,14 +720,17 @@ impl ReadRepository for Repository {
|
||||||
|
|
||||||
for r in self.backend.references_glob(pattern)? {
|
for r in self.backend.references_glob(pattern)? {
|
||||||
let r = r?;
|
let r = r?;
|
||||||
let c = r.peel_to_commit()?;
|
|
||||||
|
let Some(oid) = r.resolve()?.target() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(name) = r
|
if let Some(name) = r
|
||||||
.name()
|
.name()
|
||||||
.and_then(|n| git::RefStr::try_from_str(n).ok())
|
.and_then(|n| git::RefStr::try_from_str(n).ok())
|
||||||
.and_then(git::Qualified::from_refstr)
|
.and_then(git::Qualified::from_refstr)
|
||||||
{
|
{
|
||||||
refs.push((name.to_owned(), c.id().into()));
|
refs.push((name.to_owned(), oid.into()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(refs)
|
Ok(refs)
|
||||||
|
|
@ -761,6 +764,7 @@ impl ReadRepository for Repository {
|
||||||
.canonical(refname, self)?
|
.canonical(refname, self)?
|
||||||
.ok_or(RepositoryError::MissingBranchRule)?
|
.ok_or(RepositoryError::MissingBranchRule)?
|
||||||
.quorum(self.raw())?)
|
.quorum(self.raw())?)
|
||||||
|
.map(|(refname, _, oid)| (refname, oid))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn identity_head(&self) -> Result<Oid, RepositoryError> {
|
fn identity_head(&self) -> Result<Oid, RepositoryError> {
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,22 @@ pub fn commit(msg: &str, parents: &[git2::Oid], repo: &git2::Repository) -> git:
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create an (annotated) tag of the given commit.
|
||||||
|
pub fn tag(name: &str, message: &str, commit: git2::Oid, repo: &git2::Repository) -> git::Oid {
|
||||||
|
let target = repo
|
||||||
|
.find_object(commit, Some(git2::ObjectType::Commit))
|
||||||
|
.unwrap();
|
||||||
|
let tagger = git2::Signature::new(
|
||||||
|
"anonymous",
|
||||||
|
"anonymous@radicle.xyz",
|
||||||
|
&git2::Time::new(RADICLE_EPOCH, 0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
repo.tag(name, &target, &tagger, message, false)
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
/// Populate a repository with commits, branches and blobs.
|
/// Populate a repository with commits, branches and blobs.
|
||||||
pub fn populate(repo: &git2::Repository, scale: usize) -> Vec<git::Qualified> {
|
pub fn populate(repo: &git2::Repository, scale: usize) -> Vec<git::Qualified> {
|
||||||
assert!(
|
assert!(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue