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)
|
||||
$ 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)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ 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.
|
||||
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:
|
||||
|
||||
~~~
|
||||
|
|
@ -95,7 +95,7 @@ $ git tag v1.0-hotfix
|
|||
|
||||
``` ~alice (stderr)
|
||||
$ 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)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [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:
|
||||
|
||||
~~~
|
||||
✓ 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:
|
||||
|
|
@ -118,6 +118,13 @@ From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
|||
* [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
|
||||
`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
|
||||
|
|
@ -164,8 +171,8 @@ no quorum was found for the new tag:
|
|||
``` ~bob (stderr)
|
||||
$ git tag v2.0
|
||||
$ 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: it is recommended to find a commit to agree upon
|
||||
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
|
||||
|
|
@ -182,14 +189,16 @@ From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WR
|
|||
* [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 checkout bob/tags/v2.0
|
||||
$ git tag v2.0
|
||||
$ git tag v2.0 bob/tags/v2.0
|
||||
```
|
||||
|
||||
``` ~alice (stderr)
|
||||
$ 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)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new tag] v2.0 -> v2.0
|
||||
|
|
@ -206,7 +215,7 @@ reference is created:
|
|||
``` ~bob (stderr)
|
||||
$ git tag qa/v2.1
|
||||
$ 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)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
* [new tag] qa/v2.1 -> qa/v2.1
|
||||
|
|
@ -91,8 +91,8 @@ commit:
|
|||
|
||||
``` ~alice (stderr)
|
||||
$ 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: it is recommended to find a commit to agree upon
|
||||
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 an object (either commit or tag) to agree upon
|
||||
✓ Synced with 2 seed(s)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
d09e634..0f9bd80 master -> master
|
||||
|
|
@ -116,7 +116,7 @@ become the canonical `master`.
|
|||
|
||||
``` ~bob (stderr)
|
||||
$ 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)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
2a37862..0f9bd80 master -> master
|
||||
|
|
@ -137,7 +137,7 @@ HEAD is now at 0f9bd80 Merge remote-tracking branch 'eve/master'
|
|||
|
||||
``` ~eve (stderr)
|
||||
$ 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)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
|
||||
3a75f66..0f9bd80 master -> master
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ f2de534 Second commit
|
|||
```
|
||||
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
||||
$ 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
|
||||
f2de534..f6cff86 master -> master
|
||||
```
|
||||
|
|
@ -74,7 +74,7 @@ $ git reset --hard HEAD^ -q
|
|||
```
|
||||
``` ~alice RAD_SOCKET=/dev/null (stderr)
|
||||
$ 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
|
||||
+ f6cff86...319a7dc master -> master (forced update)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ Fast-forward
|
|||
|
||||
``` ~alice (stderr)
|
||||
$ 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)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..319a7dc master -> master
|
||||
|
|
@ -54,7 +54,7 @@ push and the new canonical head becomes the previous commit again:
|
|||
|
||||
``` ~alice (stderr)
|
||||
$ 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)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ 319a7dc...f2de534 master -> master (forced update)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ $ git commit -v -m "Define power requirements"
|
|||
|
||||
``` ~alice (stderr) RAD_SOCKET=/dev/null
|
||||
$ 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
|
||||
f2de534..3e674d1 master -> master
|
||||
```
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ $ git commit --amend --allow-empty -q -m "Amended change"
|
|||
$ git checkout master -q
|
||||
$ git merge feature/1 -q
|
||||
$ 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
|
||||
f2de534..954bcdb master -> master
|
||||
```
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ Finally, we push master and expect the patch to be merged.
|
|||
``` (stderr) RAD_SOCKET=/dev/null
|
||||
$ git push rad master
|
||||
✓ 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
|
||||
f2de534..737a10c master -> master
|
||||
```
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ When we push to `rad/master`, we automatically merge the patches:
|
|||
$ git push rad master
|
||||
✓ Patch 356f73863a8920455ff6e77cd9c805d68910551b 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
|
||||
f2de534..d6399c7 master -> master
|
||||
```
|
||||
|
|
@ -148,7 +148,7 @@ the first patch, even though they were pushed together.
|
|||
$ git reset --hard HEAD^
|
||||
$ git push -f rad
|
||||
! 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
|
||||
+ d6399c7...20aa5dd master -> master (forced update)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ $ git checkout master -q
|
|||
$ git merge feature/1
|
||||
$ git push rad master
|
||||
✓ 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
|
||||
f2de534..20aa5dd master -> master
|
||||
```
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ $ git checkout master -q
|
|||
$ git merge changes -q
|
||||
$ git push rad master
|
||||
✓ 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)
|
||||
|
||||
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/tree/b2b6432af93f8fe188e32d400263021b602cfec8
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Switched to branch 'master'
|
|||
$ git merge feature/1
|
||||
$ git push rad master
|
||||
✓ 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
|
||||
f2de534..20aa5dd master -> master
|
||||
```
|
||||
|
|
@ -50,7 +50,7 @@ When pushing, notice that we're told our patch is reverted.
|
|||
``` (stderr) RAD_SOCKET=/dev/null
|
||||
$ git push rad master --force
|
||||
! 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
|
||||
+ 20aa5dd...f2de534 master -> master (forced update)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ Fast-forward
|
|||
``` (stderr)
|
||||
$ git push rad master
|
||||
✓ 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)
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
f2de534..f567f69 master -> master
|
||||
|
|
|
|||
|
|
@ -2571,7 +2571,7 @@ fn git_tag() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn git_push_canonical_tags() {
|
||||
fn git_push_canonical_lightweight_tags() {
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node("alice");
|
||||
let bob = environment.node("bob");
|
||||
|
|
@ -2595,7 +2595,49 @@ fn git_push_canonical_tags() {
|
|||
bob.clone(rid, environment.work(&bob)).unwrap();
|
||||
formula(
|
||||
&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()
|
||||
.home(
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ pub enum Error {
|
|||
Address(#[from] address::Error),
|
||||
/// A service error.
|
||||
#[error("service error: {0}")]
|
||||
Service(#[from] service::Error),
|
||||
Service(Box<service::Error>),
|
||||
/// An I/O error.
|
||||
#[error("i/o error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
|
@ -92,6 +92,12 @@ pub enum Error {
|
|||
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.
|
||||
pub enum ControlSocket {
|
||||
/// The listener was created by binding to it.
|
||||
|
|
|
|||
|
|
@ -122,9 +122,9 @@ impl Handle {
|
|||
log::trace!(target: "worker", "Set HEAD to {}", head.new);
|
||||
}
|
||||
}
|
||||
Err(RepositoryError::Quorum(radicle::git::canonical::QuorumError::Git(e))) => {
|
||||
return Err(e.into())
|
||||
}
|
||||
Err(RepositoryError::Quorum(
|
||||
radicle::git::canonical::error::QuorumError::Git(e),
|
||||
)) => return Err(e.into()),
|
||||
Err(RepositoryError::Quorum(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;
|
||||
}
|
||||
Ok((refname, oid)) => {
|
||||
Ok((refname, _, oid)) => {
|
||||
if let Err(e) = repo.backend.reference(
|
||||
refname.clone().as_str(),
|
||||
*oid,
|
||||
|
|
|
|||
|
|
@ -206,7 +206,13 @@ pub enum Error {
|
|||
#[error(transparent)]
|
||||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
#[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)]
|
||||
|
|
@ -304,7 +310,13 @@ enum TryFetchError<'a> {
|
|||
#[error("peer fetch capacity reached; cannot initiate fetch")]
|
||||
SessionCapacityReached,
|
||||
#[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.
|
||||
|
|
|
|||
|
|
@ -113,13 +113,17 @@ pub enum Error {
|
|||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
/// Quorum error.
|
||||
#[error(transparent)]
|
||||
Quorum(#[from] radicle::git::canonical::QuorumError),
|
||||
Quorum(#[from] radicle::git::canonical::error::QuorumError),
|
||||
#[error(transparent)]
|
||||
CanonicalRefs(#[from] radicle::identity::doc::CanonicalRefsError),
|
||||
#[error(transparent)]
|
||||
PushAction(#[from] error::PushAction),
|
||||
#[error(transparent)]
|
||||
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.
|
||||
|
|
@ -280,7 +284,8 @@ pub fn run(
|
|||
let identity = stored.identity()?;
|
||||
let project = identity.project()?;
|
||||
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)?;
|
||||
|
||||
// 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
|
||||
// canonical branch.
|
||||
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) {
|
||||
Ok(quorum) => set_canonical_refs.push(quorum),
|
||||
Err(e) => canonical::io::handle_error(e, &dst, hints)?,
|
||||
|
|
@ -375,10 +386,10 @@ pub fn run(
|
|||
if !ok.is_empty() {
|
||||
let _ = stored.sign_refs(&signer)?;
|
||||
|
||||
for (refname, oid) in &set_canonical_refs {
|
||||
for (refname, kind, oid) in &set_canonical_refs {
|
||||
let print_update = || {
|
||||
eprintln!(
|
||||
"{} Canonical reference {} updated to {}",
|
||||
"{} Canonical reference {} updated to target {kind} {}",
|
||||
term::format::positive("✓"),
|
||||
term::format::secondary(refname),
|
||||
term::format::secondary(oid),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ use super::error;
|
|||
|
||||
pub(crate) struct Vote {
|
||||
did: Did,
|
||||
commit: git::Oid,
|
||||
oid: git::Oid,
|
||||
kind: git::canonical::CanonicalObjectType,
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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 {
|
||||
vote: Vote {
|
||||
did: me,
|
||||
commit: head,
|
||||
oid: head,
|
||||
kind,
|
||||
},
|
||||
canonical,
|
||||
}
|
||||
|
|
@ -46,31 +53,29 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
|||
pub fn quorum(
|
||||
mut self,
|
||||
working: &Repository,
|
||||
) -> Result<(git::Qualified<'a>, git::Oid), error::Canonical> {
|
||||
) -> Result<(git::Qualified<'a>, git::raw::ObjectType, git::Oid), error::Canonical> {
|
||||
let converges = self
|
||||
.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) {
|
||||
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) {
|
||||
Ok((cref, quorum_head)) => {
|
||||
Ok((cref, quorum_type, quorum_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
|
||||
.graph_descendant_of(*self.vote.commit, *quorum_head)
|
||||
.graph_descendant_of(*self.vote.oid, *quorum_head)
|
||||
.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 {
|
||||
Err(error::Canonical::heads_diverge(
|
||||
self.vote.commit,
|
||||
quorum_head,
|
||||
))
|
||||
Err(error::Canonical::heads_diverge(self.vote.oid, quorum_head))
|
||||
} else {
|
||||
Ok((cref, quorum_head))
|
||||
Ok((cref, quorum_type, quorum_head))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
|
|
@ -80,7 +85,7 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
|||
|
||||
pub(crate) mod io {
|
||||
use radicle::git;
|
||||
use radicle::git::canonical;
|
||||
use radicle::git::canonical::error::QuorumError;
|
||||
|
||||
use crate::push::error;
|
||||
use crate::{hint, warn};
|
||||
|
|
@ -110,19 +115,29 @@ pub(crate) mod io {
|
|||
Err(e.into())
|
||||
}
|
||||
error::Canonical::Quorum(e) => match e {
|
||||
e @ canonical::QuorumError::Diverging { .. } => {
|
||||
e @ QuorumError::DivergingCommits { .. } => {
|
||||
warn(e.to_string());
|
||||
warn("it is recommended to find a commit to agree upon");
|
||||
Ok(())
|
||||
}
|
||||
e @ canonical::QuorumError::NoCandidates { .. } => {
|
||||
e @ QuorumError::DivergingTags { .. } => {
|
||||
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(())
|
||||
}
|
||||
canonical::QuorumError::Git(err) => {
|
||||
Err(error::CanonicalUnrecoverable::Git { source: err })
|
||||
e @ QuorumError::DifferentTypes { .. } => {
|
||||
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)]
|
||||
GraphDescendant(#[from] GraphDescendant),
|
||||
#[error(transparent)]
|
||||
Converges(#[from] canonical::ConvergesError),
|
||||
Converges(#[from] canonical::error::ConvergesError),
|
||||
#[error(transparent)]
|
||||
HeadsDiverge(#[from] HeadsDiverge),
|
||||
#[error("failure while computing canonical reference: {source}")]
|
||||
|
|
@ -19,11 +19,11 @@ pub enum Canonical {
|
|||
#[error(transparent)]
|
||||
GraphDescendant(GraphDescendant),
|
||||
#[error(transparent)]
|
||||
Converges(#[from] canonical::ConvergesError),
|
||||
Converges(#[from] canonical::error::ConvergesError),
|
||||
#[error(transparent)]
|
||||
HeadsDiverge(HeadsDiverge),
|
||||
#[error(transparent)]
|
||||
Quorum(#[from] canonical::QuorumError),
|
||||
Quorum(#[from] canonical::error::QuorumError),
|
||||
}
|
||||
|
||||
impl Canonical {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
pub mod error;
|
||||
use error::*;
|
||||
|
||||
pub mod rules;
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
pub use rules::{MatchedRule, RawRule, Rules, ValidRule};
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use std::fmt;
|
||||
|
||||
use raw::ObjectType;
|
||||
use raw::Repository;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::prelude::Did;
|
||||
use crate::storage::ReadRepository;
|
||||
use crate::storage::git;
|
||||
|
||||
use super::raw;
|
||||
use super::{Oid, Qualified};
|
||||
|
|
@ -28,115 +33,70 @@ use super::{Oid, Qualified};
|
|||
pub struct Canonical<'a, 'b> {
|
||||
refname: Qualified<'a>,
|
||||
rule: &'b ValidRule,
|
||||
tips: BTreeMap<Did, Oid>,
|
||||
tips: BTreeMap<Did, (Oid, 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 commit for canonical reference '{refname}', found diverging commits {longest} and {head}, with base commit {base} and threshold {threshold}")]
|
||||
Diverging {
|
||||
refname: String,
|
||||
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),
|
||||
/// Supported Git object types for canonical computation.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CanonicalObjectType {
|
||||
/// The Git object is a commit.
|
||||
Commit,
|
||||
/// The Git object is a tag.
|
||||
Tag,
|
||||
}
|
||||
|
||||
#[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 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,
|
||||
})
|
||||
impl fmt::Display for CanonicalObjectType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CanonicalObjectType::Commit => f.write_str("commit"),
|
||||
CanonicalObjectType::Tag => f.write_str("tag"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn missing_commit(repo: PathBuf, did: Did, commit: Oid, err: raw::Error) -> Self {
|
||||
Self::MissingCommit(MissingCommit {
|
||||
repo,
|
||||
did,
|
||||
commit,
|
||||
source: err,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn invalid_commit(repo: PathBuf, did: Did, commit: Oid, err: raw::Error) -> Self {
|
||||
Self::InvalidCommit(InvalidCommit {
|
||||
repo,
|
||||
did,
|
||||
commit,
|
||||
source: err,
|
||||
})
|
||||
impl CanonicalObjectType {
|
||||
/// Construct the [`CanonicalObjectType`] from a [`git2::ObjectType`].
|
||||
pub fn new(kind: git::raw::ObjectType) -> Option<Self> {
|
||||
match kind {
|
||||
ObjectType::Commit => Some(Self::Commit),
|
||||
ObjectType::Tag => Some(Self::Tag),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Canonical<'a, 'b> {
|
||||
/// Construct the set of canonical tips given for the given `rule` and
|
||||
/// the reference `refname`.
|
||||
pub fn new<S>(repo: &S, refname: Qualified<'a>, rule: &'b ValidRule) -> Result<Self, raw::Error>
|
||||
where
|
||||
S: ReadRepository,
|
||||
{
|
||||
pub fn new(
|
||||
repo: &Repository,
|
||||
refname: Qualified<'a>,
|
||||
rule: &'b ValidRule,
|
||||
) -> Result<Self, CanonicalError> {
|
||||
let mut tips = BTreeMap::new();
|
||||
for delegate in rule.allowed().iter() {
|
||||
match repo.reference_oid(delegate, &refname) {
|
||||
Ok(tip) => {
|
||||
tips.insert(*delegate, tip);
|
||||
}
|
||||
let name = &refname.with_namespace(delegate.as_key().into());
|
||||
|
||||
let reference = match repo.find_reference(name) {
|
||||
Ok(reference) => reference,
|
||||
Err(e) if super::ext::is_not_found_err(&e) => {
|
||||
log::warn!(
|
||||
target: "radicle",
|
||||
"Missing `refs/namespaces/{}/{refname}` while calculating the canonical reference",
|
||||
delegate.as_key()
|
||||
"Missing `{name}` while calculating the canonical reference",
|
||||
);
|
||||
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 {
|
||||
refname,
|
||||
|
|
@ -145,9 +105,31 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Return the set of [`Did`]s and their [`Oid`] tip.
|
||||
pub fn tips(&self) -> impl Iterator<Item = (&Did, &Oid)> {
|
||||
self.tips.iter()
|
||||
pub fn find_object_for(
|
||||
did: &Did,
|
||||
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
|
||||
|
|
@ -165,8 +147,8 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
|||
/// 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
|
||||
/// reach a quorum.
|
||||
pub fn modify_vote(&mut self, did: Did, new: Oid) {
|
||||
self.tips.insert(did, new);
|
||||
pub fn modify_vote(&mut self, did: Did, oid: Oid, kind: CanonicalObjectType) {
|
||||
self.tips.insert(did, (oid, kind));
|
||||
}
|
||||
|
||||
/// Check that the provided `did` is part of the set of allowed
|
||||
|
|
@ -191,28 +173,116 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
|||
repo: &Repository,
|
||||
(candidate, commit): (&Did, &Oid),
|
||||
) -> 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 mut heads = self
|
||||
let heads = self
|
||||
.tips
|
||||
.iter()
|
||||
.filter_map(|(did, tip)| (did != candidate).then_some((did, tip)));
|
||||
heads.try_fold(
|
||||
Vec::with_capacity(heads.size_hint().0),
|
||||
|mut heads, (did, head)| {
|
||||
heads.push(Self::ensure_commit(*did, *head, repo)?);
|
||||
Ok::<_, ConvergesError>(heads)
|
||||
},
|
||||
)?
|
||||
|
||||
let mut objects = None;
|
||||
|
||||
for (did, (oid, _)) in 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
|
||||
.graph_ahead_behind(**commit, *head)
|
||||
.map_err(|err| ConvergesError::graph_descendant(*commit, head, err))?;
|
||||
if ahead * behind == 0 {
|
||||
return Ok(true);
|
||||
|
||||
match heads {
|
||||
None => Ok(true),
|
||||
Some(Objects::Tags(_)) => Ok(true),
|
||||
Some(Objects::Commits(heads)) => {
|
||||
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`,
|
||||
|
|
@ -222,41 +292,36 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
|||
///
|
||||
/// 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>, Oid), QuorumError> {
|
||||
let mut candidates = BTreeMap::<_, usize>::new();
|
||||
|
||||
// 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. Only commits given to the function are considered.
|
||||
for (i, head) in self.tips.values().enumerate() {
|
||||
// Add a direct vote for this head.
|
||||
*candidates.entry(*head).or_default() += 1;
|
||||
|
||||
// Compare this head to all other heads ahead of it in the list.
|
||||
for other in self.tips.values().skip(i + 1) {
|
||||
// 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;
|
||||
}
|
||||
fn quorum_commit(&self, repo: &raw::Repository) -> Result<Oid, QuorumError> {
|
||||
let mut voting =
|
||||
CommitVoting::from_targets(self.tips.values().filter_map(|(commit, kind)| {
|
||||
(*kind == CanonicalObjectType::Commit).then_some(*commit)
|
||||
}));
|
||||
while let Some(targets) = voting.next_candidate() {
|
||||
for (candidate, other) in targets {
|
||||
let base = Oid::from(repo.merge_base(*candidate, *other)?);
|
||||
voting.found_merge_base(MergeBase {
|
||||
candidate,
|
||||
other,
|
||||
base,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Keep commits which pass the threshold.
|
||||
candidates.retain(|_, votes| *votes >= self.threshold());
|
||||
let mut votes = voting.votes();
|
||||
|
||||
let (mut longest, _) = candidates.pop_first().ok_or(QuorumError::NoCandidates {
|
||||
refname: self.refname.to_string(),
|
||||
threshold: self.threshold(),
|
||||
})?;
|
||||
// Keep commits which pass the threshold.
|
||||
votes.votes_past_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
|
||||
// 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)?;
|
||||
|
||||
if base == *longest {
|
||||
|
|
@ -286,7 +351,7 @@ impl<'a, 'b> Canonical<'a, 'b> {
|
|||
// o (base)
|
||||
// |
|
||||
//
|
||||
return Err(QuorumError::Diverging {
|
||||
return Err(QuorumError::DivergingCommits {
|
||||
refname: self.refname.to_string(),
|
||||
threshold: self.threshold(),
|
||||
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 {
|
||||
(*self.rule.threshold()).into()
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_commit(from: Did, commit: Oid, working: &Repository) -> Result<Oid, ConvergesError> {
|
||||
match working.find_commit(*commit).map(|_| commit) {
|
||||
Ok(oid) => Ok(oid),
|
||||
Err(err) if err.code() == raw::ErrorCode::NotFound => Err(
|
||||
ConvergesError::missing_commit(working.path().to_path_buf(), from, commit, err),
|
||||
),
|
||||
Err(err) => Err(ConvergesError::invalid_commit(
|
||||
working.path().to_path_buf(),
|
||||
from,
|
||||
commit,
|
||||
err,
|
||||
)),
|
||||
/// Keep track of [`Votes`] for quorums involving tag objects.
|
||||
struct TagVoting {
|
||||
votes: Votes,
|
||||
}
|
||||
|
||||
impl TagVoting {
|
||||
fn from_targets(targets: impl Iterator<Item = Oid>) -> Self {
|
||||
let votes = targets.fold(Votes::default(), |mut votes, oid| {
|
||||
votes.vote(oid);
|
||||
votes
|
||||
});
|
||||
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)]
|
||||
|
|
@ -334,13 +578,19 @@ mod tests {
|
|||
threshold: usize,
|
||||
repo: &git::raw::Repository,
|
||||
) -> Result<Oid, QuorumError> {
|
||||
let tips: BTreeMap<Did, Oid> = heads
|
||||
let tips: BTreeMap<Did, (Oid, CanonicalObjectType)> = heads
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, head)| {
|
||||
let signer = Device::mock_from_seed([(i + 1) as u8; 32]);
|
||||
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();
|
||||
|
||||
|
|
@ -360,7 +610,7 @@ mod tests {
|
|||
rule: &rule,
|
||||
}
|
||||
.quorum(repo)
|
||||
.map(|(_, oid)| oid)
|
||||
.map(|(_, _, oid)| oid)
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn test_quorum() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
|
@ -447,15 +753,15 @@ mod tests {
|
|||
// C0
|
||||
assert_matches!(
|
||||
quorum(&[*c1, *c2, *b2], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*c2, *b2], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*b2, *c2], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*c2, *b2], 2, &repo),
|
||||
|
|
@ -471,7 +777,7 @@ mod tests {
|
|||
assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
|
||||
assert_matches!(
|
||||
quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
|
||||
// B2 C2 C3
|
||||
|
|
@ -500,7 +806,7 @@ mod tests {
|
|||
// C0
|
||||
assert_matches!(
|
||||
quorum(&[*c2, *b2, *a1], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
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_matches!(
|
||||
quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*a1, *a1, *c2], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
|
||||
// M2 M1
|
||||
|
|
@ -549,11 +855,11 @@ mod tests {
|
|||
assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m2], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m2, *m1], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m2], 2, &repo),
|
||||
|
|
@ -561,11 +867,11 @@ mod tests {
|
|||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m2, *c2], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *a1], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *a1], 2, &repo),
|
||||
|
|
@ -608,7 +914,7 @@ mod tests {
|
|||
// C0
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m2], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m2], 2, &repo),
|
||||
|
|
@ -624,7 +930,7 @@ mod tests {
|
|||
// C0
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m3], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m1, *m3], 2, &repo),
|
||||
|
|
@ -632,7 +938,7 @@ mod tests {
|
|||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m3, *m1], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m3, *m1], 2, &repo),
|
||||
|
|
@ -640,7 +946,7 @@ mod tests {
|
|||
);
|
||||
assert_matches!(
|
||||
quorum(&[*m3, *m2], 1, &repo),
|
||||
Err(QuorumError::Diverging { .. })
|
||||
Err(QuorumError::DivergingCommits { .. })
|
||||
);
|
||||
assert_matches!(
|
||||
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 crate::git;
|
||||
use crate::git::canonical;
|
||||
use crate::git::canonical::Canonical;
|
||||
use crate::git::fmt::{refname, RefString};
|
||||
use crate::git::refspec::QualifiedPattern;
|
||||
|
|
@ -634,9 +635,9 @@ impl Rules {
|
|||
&'a self,
|
||||
refname: Qualified<'b>,
|
||||
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() {
|
||||
Ok(Some(Canonical::new(repo, refname, rule)?))
|
||||
Ok(Some(Canonical::new(&repo.backend, refname, rule)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
|
@ -1206,7 +1207,7 @@ mod tests {
|
|||
canonical
|
||||
.quorum(&repo)
|
||||
.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)]
|
||||
GitExt(#[from] git_ext::Error),
|
||||
#[error(transparent)]
|
||||
Quorum(#[from] canonical::QuorumError),
|
||||
Quorum(#[from] canonical::error::QuorumError),
|
||||
#[error(transparent)]
|
||||
Refs(#[from] refs::Error),
|
||||
#[error("missing canonical reference rule for default branch")]
|
||||
|
|
@ -126,6 +126,8 @@ pub enum RepositoryError {
|
|||
DefaultBranchRule(#[from] doc::DefaultBranchRuleError),
|
||||
#[error("failed to get canonical reference rules: {0}")]
|
||||
CanonicalRefs(#[from] doc::CanonicalRefsError),
|
||||
#[error(transparent)]
|
||||
Canonical(#[from] canonical::error::CanonicalError),
|
||||
}
|
||||
|
||||
impl RepositoryError {
|
||||
|
|
|
|||
|
|
@ -720,14 +720,17 @@ impl ReadRepository for Repository {
|
|||
|
||||
for r in self.backend.references_glob(pattern)? {
|
||||
let r = r?;
|
||||
let c = r.peel_to_commit()?;
|
||||
|
||||
let Some(oid) = r.resolve()?.target() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(name) = r
|
||||
.name()
|
||||
.and_then(|n| git::RefStr::try_from_str(n).ok())
|
||||
.and_then(git::Qualified::from_refstr)
|
||||
{
|
||||
refs.push((name.to_owned(), c.id().into()));
|
||||
refs.push((name.to_owned(), oid.into()));
|
||||
}
|
||||
}
|
||||
Ok(refs)
|
||||
|
|
@ -761,6 +764,7 @@ impl ReadRepository for Repository {
|
|||
.canonical(refname, self)?
|
||||
.ok_or(RepositoryError::MissingBranchRule)?
|
||||
.quorum(self.raw())?)
|
||||
.map(|(refname, _, oid)| (refname, oid))
|
||||
}
|
||||
|
||||
fn identity_head(&self) -> Result<Oid, RepositoryError> {
|
||||
|
|
|
|||
|
|
@ -148,6 +148,22 @@ pub fn commit(msg: &str, parents: &[git2::Oid], repo: &git2::Repository) -> git:
|
|||
.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.
|
||||
pub fn populate(repo: &git2::Repository, scale: usize) -> Vec<git::Qualified> {
|
||||
assert!(
|
||||
|
|
|
|||
Loading…
Reference in New Issue