cli: Use `git push` to open or update patches
This adds the ability to the git-remote-helper to open and update patches via a `git push` to the magic ref `refs/patches` and `refs/heads/patches/<patch-id>`. The main change is that we had to move away from offering the `connect` capability in the remote helper, to offering `push` and `fetch` individually. To get a feel for how this works, see the `rad-patch-via-push.md` example.
This commit is contained in:
parent
f8ee109aaa
commit
fd1237b2db
|
|
@ -2093,6 +2093,7 @@ version = "0.2.0"
|
|||
dependencies = [
|
||||
"radicle",
|
||||
"radicle-crypto",
|
||||
"radicle-git-ext",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
```
|
||||
$ cd heartwood
|
||||
$ git branch -r
|
||||
rad/master
|
||||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
|
||||
```
|
||||
|
||||
```
|
||||
$ git ls-remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/heads/*'
|
||||
145e1e69bef3ad93d14946ea212249c2fa9b9828 refs/heads/alice/1
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
|
||||
```
|
||||
|
||||
``` (stderr)
|
||||
$ git fetch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new branch] alice/1 -> z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
|
||||
```
|
||||
|
||||
```
|
||||
$ git branch -r
|
||||
rad/master
|
||||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
|
||||
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
|
||||
```
|
||||
|
||||
```
|
||||
$ git rev-parse z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
|
||||
145e1e69bef3ad93d14946ea212249c2fa9b9828
|
||||
```
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
Finally, we can also delete branches with `git push`:
|
||||
|
||||
```
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/*
|
||||
145e1e69bef3ad93d14946ea212249c2fa9b9828 refs/heads/alice/1
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
|
||||
```
|
||||
|
||||
``` (stderr) RAD_SOCKET=/dev/null
|
||||
$ git push rad :alice/1
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
- [deleted] alice/1
|
||||
```
|
||||
|
||||
```
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/*
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
|
||||
```
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
```
|
||||
$ git checkout -b alice/1
|
||||
$ git commit -m "Alice's commit" --allow-empty -s
|
||||
[alice/1 87fa120] Alice's commit
|
||||
```
|
||||
|
||||
``` (stderr) RAD_SOCKET=/dev/null
|
||||
$ git push rad HEAD:alice/1
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new branch] HEAD -> alice/1
|
||||
```
|
||||
|
||||
Make sure we can't force-push without `+`:
|
||||
|
||||
``` (stderr)
|
||||
$ git commit --amend -m "Alice's amended commit" --allow-empty -s
|
||||
```
|
||||
``` (stderr) (fail)
|
||||
$ git push rad HEAD:alice/1
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
! [rejected] HEAD -> alice/1 (non-fast-forward)
|
||||
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
|
||||
hint: Updates were rejected because the tip of your current branch is behind
|
||||
hint: its remote counterpart. Integrate the remote changes (e.g.
|
||||
hint: 'git pull ...') before pushing again.
|
||||
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
|
||||
```
|
||||
|
||||
And that we can with `+`:
|
||||
|
||||
``` (stderr) RAD_SOCKET=/dev/null
|
||||
$ git push rad +HEAD:alice/1
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ 87fa120...145e1e6 HEAD -> alice/1 (forced update)
|
||||
```
|
||||
|
||||
```
|
||||
$ git branch -r -vv
|
||||
rad/alice/1 145e1e6 Alice's amended commit
|
||||
rad/master f2de534 Second commit
|
||||
```
|
||||
|
||||
List our namespaced refs:
|
||||
|
||||
```
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/heads/*'
|
||||
145e1e69bef3ad93d14946ea212249c2fa9b9828 refs/heads/alice/1
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
|
||||
```
|
||||
|
||||
List the canonical refs:
|
||||
|
||||
```
|
||||
$ git ls-remote rad
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
|
||||
```
|
||||
|
||||
```
|
||||
$ rad sync
|
||||
✓ Synced with 1 node(s)
|
||||
```
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
# Using `git push` to open patches
|
||||
|
||||
Let's checkout a branch, make a commit and push to the magic ref `refs/patches`.
|
||||
When we push to this ref, a patch is created from our commits.
|
||||
|
||||
``` (stderr)
|
||||
$ git checkout -b feature/1
|
||||
Switched to a new branch 'feature/1'
|
||||
$ git commit -a -m "Add things" -q --allow-empty
|
||||
$ git push rad HEAD:refs/patches
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
||||
We can see a patch was created:
|
||||
|
||||
```
|
||||
$ rad patch show 8f0e8ec
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add things │
|
||||
│ Patch 8f0e8ecb47a17e8f3219f33150a4092d645e4875 │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 42d894a83c9c356552a57af09ccdbd5587a99045 │
|
||||
│ Branches feature/1 │
|
||||
│ Commits ahead 1, behind 0 │
|
||||
│ Status open │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [ .. ] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
If we check our local branch, we can see its upstream is set to track a remote
|
||||
branch associated with this patch:
|
||||
|
||||
```
|
||||
$ git branch -vv
|
||||
* feature/1 42d894a [rad/patches/8f0e8ecb47a17e8f3219f33150a4092d645e4875] Add things
|
||||
master f2de534 [rad/master] Second commit
|
||||
```
|
||||
|
||||
Let's check that it's up to date with our local head:
|
||||
|
||||
```
|
||||
$ git status --short --branch
|
||||
## feature/1...rad/patches/8f0e8ecb47a17e8f3219f33150a4092d645e4875
|
||||
$ git fetch
|
||||
$ git push
|
||||
```
|
||||
|
||||
And let's look at our local and remote refs:
|
||||
|
||||
```
|
||||
$ git show-ref
|
||||
42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/feature/1
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
|
||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/remotes/rad/master
|
||||
42d894a83c9c356552a57af09ccdbd5587a99045 refs/remotes/rad/patches/8f0e8ecb47a17e8f3219f33150a4092d645e4875
|
||||
```
|
||||
```
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/heads/patches/*'
|
||||
42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/patches/8f0e8ecb47a17e8f3219f33150a4092d645e4875
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/cobs/*'
|
||||
8f0e8ecb47a17e8f3219f33150a4092d645e4875 refs/cobs/xyz.radicle.patch/8f0e8ecb47a17e8f3219f33150a4092d645e4875
|
||||
```
|
||||
|
||||
We can also create patches by pushing to the `rad/patches` remote. It's a bit
|
||||
simpler:
|
||||
|
||||
``` (stderr)
|
||||
$ git checkout -b feature/2 -q
|
||||
$ git commit -a -m "Add more things" -q --allow-empty
|
||||
$ git push rad/patches
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
||||
We see both branches with upstreams now:
|
||||
|
||||
```
|
||||
$ git branch -vv
|
||||
feature/1 42d894a [rad/patches/8f0e8ecb47a17e8f3219f33150a4092d645e4875] Add things
|
||||
* feature/2 b94a835 [rad/patches/8678aafff1d1e28430952abf431e60b87e28023c] Add more things
|
||||
master f2de534 [rad/master] Second commit
|
||||
```
|
||||
|
||||
And both patches:
|
||||
|
||||
```
|
||||
$ rad patch
|
||||
╭────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ● ID Title Author Head + - Updated │
|
||||
├────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● 8678aaf Add more things z6MknSL…StBU8Vi (you) b94a835 +0 -0 [ ... ] │
|
||||
│ ● 8f0e8ec Add things z6MknSL…StBU8Vi (you) 42d894a +0 -0 [ ... ] │
|
||||
╰────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
Note that we can't fetch from `rad/patches`:
|
||||
|
||||
``` (stderr) (fail)
|
||||
$ git fetch rad/patches
|
||||
fatal: couldn't find remote ref refs/patches
|
||||
```
|
||||
|
||||
To update our patch, we simply push commits to the upstream branch:
|
||||
|
||||
```
|
||||
$ git commit -a -m "Improve code" -q --allow-empty
|
||||
```
|
||||
|
||||
``` (stderr)
|
||||
$ git push
|
||||
4b6618a6ccb0b406659364a70a00bb60e4cd7cf0
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
b94a835..662843e feature/2 -> patches/8678aafff1d1e28430952abf431e60b87e28023c
|
||||
```
|
||||
|
||||
This last `git push` worked without specifying an upstream branch despite the
|
||||
local branch having a different name than the remote. This is because Radicle
|
||||
configures repositories upon `rad init` with `push.default = upstream`:
|
||||
|
||||
```
|
||||
$ git config --local --get push.default
|
||||
upstream
|
||||
```
|
||||
|
||||
This allows for pushing to the remote patch branch without using the full
|
||||
`<src>:<dst>` syntax.
|
||||
|
||||
We can then see that the patch head has moved:
|
||||
|
||||
```
|
||||
$ rad patch show 8678aaf
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add more things │
|
||||
│ Patch 8678aafff1d1e28430952abf431e60b87e28023c │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 662843ed81e76efa69d7901fb7bdd775043015d0 │
|
||||
│ Branches feature/2 │
|
||||
│ Commits ahead 3, behind 0 │
|
||||
│ Status open │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [ ... ] │
|
||||
│ ↑ updated to 4b6618a6ccb0b406659364a70a00bb60e4cd7cf0 (662843e) [ ... ] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
And we can check that all the refs are properly updated in our repository:
|
||||
|
||||
```
|
||||
$ git rev-parse HEAD
|
||||
662843ed81e76efa69d7901fb7bdd775043015d0
|
||||
```
|
||||
|
||||
```
|
||||
$ git status --short --branch
|
||||
## feature/2...rad/patches/8678aafff1d1e28430952abf431e60b87e28023c
|
||||
```
|
||||
|
||||
```
|
||||
$ git rev-parse refs/remotes/rad/patches/8678aafff1d1e28430952abf431e60b87e28023c
|
||||
662843ed81e76efa69d7901fb7bdd775043015d0
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/patches/8678aafff1d1e28430952abf431e60b87e28023c
|
||||
662843ed81e76efa69d7901fb7bdd775043015d0 refs/heads/patches/8678aafff1d1e28430952abf431e60b87e28023c
|
||||
```
|
||||
|
||||
## Force push
|
||||
|
||||
Sometimes, it's necessary to force-push a patch update. For example, if we amended
|
||||
the commit and want the updated patch to reflect that.
|
||||
|
||||
Let's try.
|
||||
|
||||
```
|
||||
$ git commit --amend -m "Amended commit" --allow-empty
|
||||
[feature/2 3507cd5] Amended commit
|
||||
Date: [..]
|
||||
```
|
||||
|
||||
Now let's push to the patch head.
|
||||
|
||||
``` (stderr) (fail)
|
||||
$ git push
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
! [rejected] feature/2 -> patches/8678aafff1d1e28430952abf431e60b87e28023c (non-fast-forward)
|
||||
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
|
||||
hint: Updates were rejected because a pushed branch tip is behind its remote
|
||||
hint: counterpart. Check out this branch and integrate the remote changes
|
||||
hint: (e.g. 'git pull ...') before pushing again.
|
||||
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
|
||||
```
|
||||
|
||||
The push fails because it's not a fast-forward update. To remedy this, we can
|
||||
use `--force` to force the update.
|
||||
|
||||
``` (stderr)
|
||||
$ git push --force
|
||||
983f2d21c9fbe91c21ddfbcd3e3d6cd13db0a944
|
||||
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
|
||||
+ 662843e...3507cd5 feature/2 -> patches/8678aafff1d1e28430952abf431e60b87e28023c (forced update)
|
||||
```
|
||||
|
||||
That worked. We can see the new revision if we call `rad patch show`:
|
||||
|
||||
```
|
||||
$ rad patch show 8678aaf
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Title Add more things │
|
||||
│ Patch 8678aafff1d1e28430952abf431e60b87e28023c │
|
||||
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
|
||||
│ Head 3507cd57811fe5f21f6a0a610a1ac8068b3a5d9f │
|
||||
│ Branches feature/2 │
|
||||
│ Commits ahead 3, behind 0 │
|
||||
│ Status open │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) 4 months ago │
|
||||
│ ↑ updated to 4b6618a6ccb0b406659364a70a00bb60e4cd7cf0 (662843e) 4 months ago │
|
||||
│ ↑ updated to 983f2d21c9fbe91c21ddfbcd3e3d6cd13db0a944 (3507cd5) 4 months ago │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
|
@ -9,9 +9,10 @@ Now, we can see that there is a new remote in the list of remotes:
|
|||
|
||||
```
|
||||
$ rad remote list
|
||||
bob z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (fetch)
|
||||
rad the canonical upstream (fetch)
|
||||
rad z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
|
||||
bob z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (fetch)
|
||||
rad (canonical upstream) (fetch)
|
||||
rad z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
|
||||
rad/patches (patches upstream) (push)
|
||||
```
|
||||
|
||||
You can see both `bob` and `rad` as remotes. The `rad` remote is our personal remote of the project.
|
||||
|
|
|
|||
|
|
@ -62,6 +62,13 @@ $ rad patch show 5f0a547f7a91bf002bb0542035a647fd5af134a5
|
|||
╰─────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
We can also confirm that the patch branch is in storage:
|
||||
|
||||
```
|
||||
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk refs/heads/flux-capacitor-power
|
||||
3e674d1a1df90807e934f9ae5da2591dd6848a33 refs/heads/flux-capacitor-power
|
||||
```
|
||||
|
||||
Wait, let's add a README too! Just for fun.
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -16,7 +16,13 @@ peer. Upcoming versions of radicle will not require this step.
|
|||
```
|
||||
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
|
||||
✓ Remote bob added with rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
```
|
||||
|
||||
``` (stderr)
|
||||
$ git fetch bob
|
||||
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
* [new branch] flux-capacitor-power -> bob/flux-capacitor-power
|
||||
* [new branch] master -> bob/master
|
||||
```
|
||||
|
||||
The contributor's changes are now visible to us.
|
||||
|
|
|
|||
|
|
@ -239,9 +239,9 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
|
|||
// Setup eg. `master` -> `rad/master`
|
||||
radicle::git::set_upstream(
|
||||
&repo,
|
||||
&radicle::rad::REMOTE_NAME,
|
||||
&*radicle::rad::REMOTE_NAME,
|
||||
proj.default_branch(),
|
||||
&radicle::git::refs::workdir::branch(proj.default_branch()),
|
||||
radicle::git::refs::workdir::branch(proj.default_branch()),
|
||||
)?;
|
||||
} else {
|
||||
push_cmd = format!("git push {}", *radicle::rad::REMOTE_NAME);
|
||||
|
|
|
|||
|
|
@ -60,11 +60,12 @@ pub fn run(
|
|||
let (target_ref, target_oid) = get_merge_target(storage, &head_branch)?;
|
||||
|
||||
if head_branch.upstream().is_err() {
|
||||
// TODO(cloudhead): Disable this when invoked from remote helper.
|
||||
radicle::git::set_upstream(
|
||||
workdir,
|
||||
&radicle::rad::REMOTE_NAME,
|
||||
&*radicle::rad::REMOTE_NAME,
|
||||
branch_name(&head_branch)?,
|
||||
&head_branch_name,
|
||||
head_branch_name,
|
||||
)?;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use radicle::rad;
|
||||
use radicle_term::{Element, Table};
|
||||
|
||||
use crate::git;
|
||||
|
|
@ -11,13 +12,22 @@ pub fn run(repo: &git::Repository) -> anyhow::Result<()> {
|
|||
let Some(url) = url else {
|
||||
continue;
|
||||
};
|
||||
let nid = url.namespace.map_or(
|
||||
term::format::dim("the canonical upstream".to_string()).italic(),
|
||||
|namespace| term::format::tertiary(namespace.to_string()),
|
||||
);
|
||||
|
||||
let description = if r.name == rad::PATCHES_REMOTE_NAME.as_str() {
|
||||
if dir == "fetch" {
|
||||
// Fetching from the patches remote is not allowed.
|
||||
continue;
|
||||
}
|
||||
term::format::dim("(patches upstream)".to_string()).italic()
|
||||
} else {
|
||||
url.namespace.map_or(
|
||||
term::format::dim("(canonical upstream)".to_string()).italic(),
|
||||
|namespace| term::format::tertiary(namespace.to_string()),
|
||||
)
|
||||
};
|
||||
table.push([
|
||||
term::format::bold(r.name.clone()),
|
||||
nid,
|
||||
description,
|
||||
term::format::parens(term::format::secondary(dir.to_owned())),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ fn announce(rid: Id, mut node: Node, timeout: time::Duration) -> anyhow::Result<
|
|||
let mut seeds = seeds.connected().collect::<BTreeSet<_>>();
|
||||
|
||||
if seeds.is_empty() {
|
||||
term::info!("Not connected to any seeds");
|
||||
term::info!("Not connected to any seeds.");
|
||||
return Ok(());
|
||||
}
|
||||
node.announce_refs(rid)?;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ impl<'a> SetupRemote<'a> {
|
|||
let local_branch = radicle::git::refs::workdir::branch(
|
||||
node_ref.join(&self.default_branch).as_refstr(),
|
||||
);
|
||||
radicle::git::set_upstream(self.repo, &node.to_string(), &branch_name, &local_branch)?;
|
||||
radicle::git::set_upstream(self.repo, node.to_string(), &branch_name, local_branch)?;
|
||||
|
||||
return Ok(Some((remote, branch_name)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,6 +292,28 @@ fn rad_patch_draft() {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_patch_via_push() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let profile = environment.profile("alice");
|
||||
let working = tempfile::tempdir().unwrap();
|
||||
let home = &profile.home;
|
||||
|
||||
// Setup a test repository.
|
||||
fixtures::repository(working.path());
|
||||
|
||||
test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
|
||||
test(
|
||||
"examples/rad-patch-via-push.md",
|
||||
working.path(),
|
||||
Some(home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_rm() {
|
||||
let mut environment = Environment::new();
|
||||
|
|
@ -791,6 +813,60 @@ fn rad_remote() {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_push_and_pull() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let alice = environment.node("alice");
|
||||
let bob = environment.node("bob");
|
||||
let working = environment.tmp().join("working");
|
||||
|
||||
fixtures::repository(working.join("alice"));
|
||||
|
||||
test(
|
||||
"examples/rad-init.md",
|
||||
working.join("alice"),
|
||||
Some(&alice.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let alice = alice.spawn(Config::default());
|
||||
let mut bob = bob.spawn(Config::default());
|
||||
|
||||
bob.connect(&alice).converge([&alice]);
|
||||
|
||||
test(
|
||||
"examples/rad-clone.md",
|
||||
&working.join("bob"),
|
||||
Some(&bob.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
test(
|
||||
"examples/git/git-push.md",
|
||||
&working.join("alice"),
|
||||
Some(&alice.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
test(
|
||||
"examples/git/git-pull.md",
|
||||
&working.join("bob"),
|
||||
Some(&bob.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
test(
|
||||
"examples/git/git-push-delete.md",
|
||||
&working.join("alice"),
|
||||
Some(&alice.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_workflow() {
|
||||
let mut environment = Environment::new();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
thiserror = "1"
|
||||
radicle-git-ext = { version = "0" }
|
||||
|
||||
[dependencies.radicle]
|
||||
path = "../radicle"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use radicle::git;
|
||||
use radicle::storage::git::transport::local::Url;
|
||||
use radicle::storage::ReadRepository;
|
||||
|
||||
use crate::read_line;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Invalid command received.
|
||||
#[error("invalid command `{0}`")]
|
||||
InvalidCommand(String),
|
||||
/// I/O error.
|
||||
#[error("i/o error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
/// Invalid reference name.
|
||||
#[error("invalid ref: {0}")]
|
||||
InvalidRef(#[from] radicle::git::fmt::Error),
|
||||
/// Git error.
|
||||
#[error("git: {0}")]
|
||||
Git(#[from] git::raw::Error),
|
||||
}
|
||||
|
||||
/// Run a git fetch command.
|
||||
pub fn run<R: ReadRepository>(
|
||||
mut refs: Vec<String>,
|
||||
working: &Path,
|
||||
url: Url,
|
||||
stored: R,
|
||||
stdin: &io::Stdin,
|
||||
) -> Result<(), Error> {
|
||||
// Read all the `fetch` lines.
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
let tokens = read_line(stdin, &mut line)?;
|
||||
match tokens.as_slice() {
|
||||
["fetch", _oid, refstr] => {
|
||||
refs.push(refstr.to_string());
|
||||
}
|
||||
// An empty line means end of input.
|
||||
[] => break,
|
||||
// Once the first `fetch` command is received, we don't expect anything else.
|
||||
_ => return Err(Error::InvalidCommand(line.trim().to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
// Verify them and prepare the final refspecs.
|
||||
let mut refspecs = Vec::new();
|
||||
for refstr in refs {
|
||||
let refstr = git::RefString::try_from(refstr)?;
|
||||
if let Some(nid) = url.namespace {
|
||||
refspecs.push(nid.to_namespace().join(refstr).to_string());
|
||||
} else {
|
||||
refspecs.push(refstr.to_string());
|
||||
};
|
||||
}
|
||||
|
||||
git::raw::Repository::open(working)?
|
||||
.remote_anonymous(&git::url::File::new(stored.path()).to_string())?
|
||||
.fetch(&refspecs, None, None)?;
|
||||
|
||||
// Nb. An empty line means we're done.
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,34 +1,25 @@
|
|||
#![allow(clippy::collapsible_if)]
|
||||
use std::os::fd::{AsRawFd, FromRawFd};
|
||||
//! The Radicle Git remote helper.
|
||||
//!
|
||||
//! Communication with the user is done via `stderr` (`eprintln`).
|
||||
//! Communication with Git tooling is done via `stdout` (`println`).
|
||||
mod fetch;
|
||||
mod list;
|
||||
mod push;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{env, io, process};
|
||||
use std::{env, io};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use radicle::crypto::PublicKey;
|
||||
use radicle::node::Handle;
|
||||
use radicle::git;
|
||||
use radicle::storage::git::transport::local::{Url, UrlError};
|
||||
use radicle::storage::{ReadRepository, WriteRepository, WriteStorage};
|
||||
|
||||
/// The service invoked by git on the remote repository, during a push.
|
||||
const GIT_RECEIVE_PACK: &str = "git-receive-pack";
|
||||
/// The service invoked by git on the remote repository, during a fetch.
|
||||
const GIT_UPLOAD_PACK: &str = "git-upload-pack";
|
||||
use radicle::storage::{ReadRepository, WriteStorage};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Remote repository not found (or empty).
|
||||
#[error("remote repository `{0}` not found")]
|
||||
RepositoryNotFound(PathBuf),
|
||||
/// Secret key is not registered, eg. with ssh-agent.
|
||||
#[error("public key `{0}` is not registered with ssh-agent")]
|
||||
KeyNotRegistered(PublicKey),
|
||||
/// Public key doesn't match the remote namespace we're pushing to.
|
||||
#[error("public key `{0}` does not match remote namespace")]
|
||||
KeyMismatch(PublicKey),
|
||||
/// No public key is given
|
||||
#[error("no public key given as a remote namespace, perhaps you are attempting to push to restricted refs")]
|
||||
NoKey,
|
||||
/// Invalid command received.
|
||||
#[error("invalid command `{0}`")]
|
||||
InvalidCommand(String),
|
||||
|
|
@ -38,12 +29,31 @@ pub enum Error {
|
|||
/// Error with the remote url.
|
||||
#[error("invalid remote url: {0}")]
|
||||
RemoteUrl(#[from] UrlError),
|
||||
/// I/O error.
|
||||
#[error("i/o error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
/// The `GIT_DIR` env var is not set.
|
||||
#[error("the `GIT_DIR` environment variable is not set")]
|
||||
NoGitDir,
|
||||
/// Git error.
|
||||
#[error("git: {0}")]
|
||||
Git(#[from] git::raw::Error),
|
||||
/// Storage error.
|
||||
#[error(transparent)]
|
||||
Storage(#[from] radicle::storage::Error),
|
||||
/// Fetch error.
|
||||
#[error(transparent)]
|
||||
Fetch(#[from] fetch::Error),
|
||||
/// Push error.
|
||||
#[error(transparent)]
|
||||
Push(#[from] push::Error),
|
||||
/// List error.
|
||||
#[error(transparent)]
|
||||
List(#[from] list::Error),
|
||||
}
|
||||
|
||||
/// Run the radicle remote helper using the given profile.
|
||||
pub fn run(profile: radicle::Profile) -> Result<(), Box<dyn std::error::Error + 'static>> {
|
||||
// `GIT_DIR` is expected to be set, though we aren't using it right now.
|
||||
let _git_dir = env::var("GIT_DIR").map(PathBuf::from)?;
|
||||
pub fn run(profile: radicle::Profile) -> Result<(), Error> {
|
||||
let url: Url = {
|
||||
let args = env::args().skip(1).take(2).collect::<Vec<_>>();
|
||||
|
||||
|
|
@ -52,104 +62,87 @@ pub fn run(profile: radicle::Profile) -> Result<(), Box<dyn std::error::Error +
|
|||
[_, url] => url.parse(),
|
||||
|
||||
_ => {
|
||||
return Err(Error::InvalidArguments(args).into());
|
||||
return Err(Error::InvalidArguments(args));
|
||||
}
|
||||
}
|
||||
}?;
|
||||
|
||||
let proj = profile.storage.repository_mut(url.repo)?;
|
||||
if proj.is_empty()? {
|
||||
return Err(Error::RepositoryNotFound(proj.path().to_path_buf()).into());
|
||||
let stored = profile.storage.repository_mut(url.repo)?;
|
||||
if stored.is_empty()? {
|
||||
return Err(Error::RepositoryNotFound(stored.path().to_path_buf()));
|
||||
}
|
||||
|
||||
// `GIT_DIR` is expected to be set by Git tooling, and points to the working copy.
|
||||
let working = env::var("GIT_DIR")
|
||||
.map(PathBuf::from)
|
||||
.map_err(|_| Error::NoGitDir)?;
|
||||
|
||||
let stdin = io::stdin();
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
let read = stdin.read_line(&mut line)?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
let tokens = read_line(&stdin, &mut line)?;
|
||||
|
||||
let tokens = line.trim().split(' ').collect::<Vec<_>>();
|
||||
match tokens.as_slice() {
|
||||
// First we are asked about capabilities.
|
||||
["capabilities"] => {
|
||||
println!("connect");
|
||||
println!("option");
|
||||
println!("push"); // Implies `list` command.
|
||||
println!("fetch");
|
||||
println!();
|
||||
}
|
||||
// Since we send a `connect` back, this is what is requested next.
|
||||
["connect", service] => {
|
||||
// Don't allow push if either of these conditions is true:
|
||||
//
|
||||
// 1. Our key is not in ssh-agent, which means we won't be able to sign the refs.
|
||||
// 2. Our key is not the one loaded in the profile, which means that the signed refs
|
||||
// won't match the remote we're pushing to.
|
||||
// 3. The URL namespace is not set, which is used for fetching canonical refs.
|
||||
let signer = if *service == GIT_RECEIVE_PACK {
|
||||
match url.namespace {
|
||||
Some(namespace) => {
|
||||
if profile.public_key != namespace {
|
||||
return Err(Error::KeyMismatch(profile.public_key).into());
|
||||
}
|
||||
}
|
||||
None => return Err(Error::NoKey.into()),
|
||||
}
|
||||
|
||||
let signer = profile.signer()?;
|
||||
|
||||
Some(signer)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if *service == GIT_UPLOAD_PACK {
|
||||
// TODO: Fetch from network.
|
||||
}
|
||||
println!(); // Empty line signifies connection is established.
|
||||
|
||||
let mut child = process::Command::new(service)
|
||||
.arg(proj.path())
|
||||
.env("GIT_DIR", proj.path())
|
||||
.env(
|
||||
"GIT_NAMESPACE",
|
||||
url.namespace.map(|ns| ns.to_string()).unwrap_or_default(),
|
||||
)
|
||||
.stdout(process::Stdio::inherit())
|
||||
.stderr(process::Stdio::inherit())
|
||||
.stdin(process::Stdio::inherit())
|
||||
.spawn()?;
|
||||
|
||||
if child.wait()?.success() && *service == GIT_RECEIVE_PACK {
|
||||
if let Some(signer) = signer {
|
||||
proj.sign_refs(&signer)?;
|
||||
proj.set_head()?;
|
||||
// Connect to local node and announce refs to the network.
|
||||
// If our node is not running, we simply skip this step, as the
|
||||
// refs will be announced eventually, when the node restarts.
|
||||
if radicle::Node::new(profile.socket()).is_running() {
|
||||
let stderr = io::stderr().as_raw_fd();
|
||||
|
||||
process::Command::new("rad")
|
||||
.arg("sync")
|
||||
.arg(proj.id.to_string())
|
||||
.arg("--verbose")
|
||||
.stdout(unsafe { process::Stdio::from_raw_fd(stderr) })
|
||||
.stderr(process::Stdio::inherit())
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
["option", "verbosity"] => {
|
||||
println!("ok");
|
||||
}
|
||||
["option", "push-option", _opt] => {
|
||||
println!("unsupported");
|
||||
}
|
||||
["option", "progress", ..] => {
|
||||
println!("unsupported");
|
||||
}
|
||||
["option", ..] => {
|
||||
println!("unsupported");
|
||||
}
|
||||
["fetch", _oid, refstr] => {
|
||||
return fetch::run(vec![refstr.to_string()], &working, url, stored, &stdin)
|
||||
.map_err(Error::from);
|
||||
}
|
||||
["push", refspec] => {
|
||||
return push::run(
|
||||
vec![refspec.to_string()],
|
||||
&working,
|
||||
url,
|
||||
stored,
|
||||
&profile,
|
||||
&stdin,
|
||||
)
|
||||
.map_err(Error::from);
|
||||
}
|
||||
["list"] => {
|
||||
list::for_fetch(&url, &stored)?;
|
||||
}
|
||||
["list", "for-push"] => {
|
||||
list::for_push(&profile, &stored)?;
|
||||
}
|
||||
// An empty line means end of input.
|
||||
[] => {
|
||||
break;
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::InvalidCommand(line).into());
|
||||
return Err(Error::InvalidCommand(line.trim().to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read one line from stdin, and split it into tokens.
|
||||
pub(crate) fn read_line<'a>(stdin: &io::Stdin, line: &'a mut String) -> io::Result<Vec<&'a str>> {
|
||||
line.clear();
|
||||
|
||||
let read = stdin.read_line(line)?;
|
||||
if read == 0 {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let line = line.trim();
|
||||
let tokens = line.split(' ').filter(|t| !t.is_empty()).collect();
|
||||
|
||||
Ok(tokens)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
use thiserror::Error;
|
||||
|
||||
use radicle::git;
|
||||
use radicle::storage::git::transport::local::Url;
|
||||
use radicle::storage::ReadRepository;
|
||||
use radicle::Profile;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Storage error.
|
||||
#[error(transparent)]
|
||||
Storage(#[from] radicle::storage::Error),
|
||||
/// Identity error.
|
||||
#[error(transparent)]
|
||||
Identity(#[from] radicle::identity::IdentityError),
|
||||
/// Git error.
|
||||
#[error(transparent)]
|
||||
Git(#[from] radicle::git::ext::Error),
|
||||
}
|
||||
|
||||
/// List refs for fetching (`git fetch` and `git ls-remote`).
|
||||
pub fn for_fetch<R: ReadRepository>(url: &Url, stored: &R) -> Result<(), Error> {
|
||||
if let Some(namespace) = url.namespace {
|
||||
// Listing namespaced refs.
|
||||
for (name, oid) in stored.references_of(&namespace)? {
|
||||
println!("{oid} {name}");
|
||||
}
|
||||
} else {
|
||||
// Listing canonical refs.
|
||||
// We skip over `refs/rad/*`, since those are not meant to be fetched into a working copy.
|
||||
for glob in [
|
||||
git::refspec::pattern!("refs/heads/*"),
|
||||
git::refspec::pattern!("refs/tags/*"),
|
||||
] {
|
||||
for (name, oid) in stored.references_glob(&glob)? {
|
||||
println!("{oid} {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List refs for pushing (`git push`).
|
||||
pub fn for_push<R: ReadRepository>(profile: &Profile, stored: &R) -> Result<(), Error> {
|
||||
// Only our own refs can be pushed to.
|
||||
for (name, oid) in stored.references_of(profile.id())? {
|
||||
println!("{oid} {name}");
|
||||
}
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::os::fd::{AsRawFd, FromRawFd};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::{io, process};
|
||||
|
||||
use radicle::storage::git::cob::object::ParseObjectId;
|
||||
use thiserror::Error;
|
||||
|
||||
use radicle::crypto::PublicKey;
|
||||
use radicle::node::{Handle, NodeId};
|
||||
use radicle::storage::git::transport::local::Url;
|
||||
use radicle::storage::WriteRepository;
|
||||
use radicle::Profile;
|
||||
use radicle::{git, rad};
|
||||
|
||||
use crate::read_line;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Public key doesn't match the remote namespace we're pushing to.
|
||||
#[error("public key `{0}` does not match remote namespace")]
|
||||
KeyMismatch(PublicKey),
|
||||
/// No public key is given
|
||||
#[error("no public key given as a remote namespace, perhaps you are attempting to push to restricted refs")]
|
||||
NoKey,
|
||||
/// Invalid command received.
|
||||
#[error("invalid command `{0}`")]
|
||||
InvalidCommand(String),
|
||||
/// I/O error.
|
||||
#[error("i/o error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
/// A command exited with an error code.
|
||||
#[error("command '{0}' failed with status {1}")]
|
||||
CommandFailed(String, i32),
|
||||
/// Invalid reference name.
|
||||
#[error("invalid ref: {0}")]
|
||||
InvalidRef(#[from] radicle::git::fmt::Error),
|
||||
/// Git error.
|
||||
#[error("git: {0}")]
|
||||
Git(#[from] git::raw::Error),
|
||||
/// Storage error.
|
||||
#[error(transparent)]
|
||||
Storage(#[from] radicle::storage::Error),
|
||||
/// Profile error.
|
||||
#[error(transparent)]
|
||||
Profile(#[from] radicle::profile::Error),
|
||||
/// Identity error.
|
||||
#[error(transparent)]
|
||||
Identity(#[from] radicle::identity::IdentityError),
|
||||
/// Parse error for object IDs.
|
||||
#[error(transparent)]
|
||||
ParseObjectId(#[from] ParseObjectId),
|
||||
}
|
||||
|
||||
enum Command {
|
||||
Push(git::Refspec<git::RefString, git::RefString>),
|
||||
Delete(git::RefString),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Return the destination refname.
|
||||
fn dst(&self) -> &git::RefStr {
|
||||
match self {
|
||||
Self::Push(rs) => rs.dst.as_refstr(),
|
||||
Self::Delete(rs) => rs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Command {
|
||||
type Err = git::ext::ref_format::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let Some((src, dst)) = s.split_once(':') else {
|
||||
return Err(git::ext::ref_format::Error::Empty);
|
||||
};
|
||||
let dst = git::RefString::try_from(dst)?;
|
||||
|
||||
if src.is_empty() {
|
||||
Ok(Self::Delete(dst))
|
||||
} else {
|
||||
let (src, force) = if let Some(stripped) = src.strip_prefix('+') {
|
||||
(stripped, true)
|
||||
} else {
|
||||
(src, false)
|
||||
};
|
||||
let src = git::RefString::try_from(src)?;
|
||||
|
||||
Ok(Self::Push(git::Refspec { src, dst, force }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a git push command.
|
||||
pub fn run<R: WriteRepository>(
|
||||
mut specs: Vec<String>,
|
||||
working: &Path,
|
||||
url: Url,
|
||||
stored: R,
|
||||
profile: &Profile,
|
||||
stdin: &io::Stdin,
|
||||
) -> Result<(), Error> {
|
||||
// Don't allow push if either of these conditions is true:
|
||||
//
|
||||
// 1. Our key is not in ssh-agent, which means we won't be able to sign the refs.
|
||||
// 2. Our key is not the one loaded in the profile, which means that the signed refs
|
||||
// won't match the remote we're pushing to.
|
||||
// 3. The URL namespace is not set.
|
||||
let nid = url.namespace.ok_or(Error::NoKey).and_then(|ns| {
|
||||
(profile.public_key == ns)
|
||||
.then_some(ns)
|
||||
.ok_or(Error::KeyMismatch(profile.public_key))
|
||||
})?;
|
||||
let signer = profile.signer()?;
|
||||
let mut line = String::new();
|
||||
let mut ok = HashSet::new();
|
||||
|
||||
// Read all the `push` lines.
|
||||
loop {
|
||||
let tokens = read_line(stdin, &mut line)?;
|
||||
match tokens.as_slice() {
|
||||
["push", spec] => {
|
||||
specs.push(spec.to_string());
|
||||
}
|
||||
// An empty line means end of input.
|
||||
[] => break,
|
||||
// Once the first `push` command is received, we don't expect anything else.
|
||||
_ => return Err(Error::InvalidCommand(line.trim().to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
// For each refspec, push a ref or delete a ref.
|
||||
for spec in specs {
|
||||
let Ok(cmd) = Command::from_str(&spec) else {
|
||||
return Err(Error::InvalidCommand(format!("push {spec}")));
|
||||
};
|
||||
let result = match &cmd {
|
||||
Command::Delete(dst) => {
|
||||
// Delete refs.
|
||||
let refname = nid.to_namespace().join(dst);
|
||||
stored
|
||||
.raw()
|
||||
.find_reference(&refname)
|
||||
.and_then(|mut r| r.delete())
|
||||
.map_err(Error::from)
|
||||
}
|
||||
Command::Push(git::Refspec { src, dst, force }) => {
|
||||
let working = git::raw::Repository::open(working)?;
|
||||
let stored = stored.raw();
|
||||
|
||||
if let Some(oid) = dst.strip_prefix(git::refname!("refs/heads/patches")) {
|
||||
let oid = git::Oid::from_str(oid)?;
|
||||
|
||||
patch_update(src, dst, *force, &oid, &nid, &working, stored)
|
||||
} else if dst == &*rad::PATCHES_REFNAME {
|
||||
patch_open(src, &nid, &working, stored)
|
||||
} else {
|
||||
push_ref(src, dst, *force, &nid, &working, stored)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
// Let Git tooling know that this ref has been pushed.
|
||||
Ok(()) => {
|
||||
println!("ok {}", cmd.dst());
|
||||
ok.insert(spec);
|
||||
}
|
||||
// Let Git tooling know that there was an error pushing the ref.
|
||||
Err(e) => println!("error {} {e}", cmd.dst()),
|
||||
}
|
||||
}
|
||||
|
||||
// Sign refs and sync if at least one ref pushed successfully.
|
||||
if !ok.is_empty() {
|
||||
stored.sign_refs(&signer)?;
|
||||
stored.set_head()?;
|
||||
|
||||
// Connect to local node and announce refs to the network.
|
||||
// If our node is not running, we simply skip this step, as the
|
||||
// refs will be announced eventually, when the node restarts.
|
||||
if radicle::Node::new(profile.socket()).is_running() {
|
||||
let rid = stored.id().to_string();
|
||||
let stderr = io::stderr().as_raw_fd();
|
||||
// Nb. allow this to fail. The push to local storage was still successful.
|
||||
execute("rad", ["sync", &rid, "--verbose"], unsafe {
|
||||
process::Stdio::from_raw_fd(stderr)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
// Done.
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open a new patch.
|
||||
fn patch_open(
|
||||
src: &git::RefStr,
|
||||
nid: &NodeId,
|
||||
working: &git::raw::Repository,
|
||||
stored: &git::raw::Repository,
|
||||
) -> Result<(), Error> {
|
||||
let reference = working.find_reference(src.as_str())?;
|
||||
let commit = reference.peel_to_commit()?;
|
||||
let dst = &*git::refs::storage::staging::patch(nid, commit.id());
|
||||
|
||||
// Before creating the patch, we must push the associated git objects to storage.
|
||||
// Unfortunately, we don't have an easy way to transfer the missing objects without
|
||||
// creating a temporary reference on the remote. The temporary reference is deleted
|
||||
// once the patch is open, or in case of error.
|
||||
//
|
||||
// In case the reference is not properly deleted, the next attempt to open a patch should
|
||||
// not fail, since the reference will already exist with the correct OID.
|
||||
push_ref(src, dst, false, nid, working, stored)?;
|
||||
|
||||
let result = match execute(
|
||||
"rad",
|
||||
["patch", "open", "--quiet", "--no-push"],
|
||||
process::Stdio::piped(),
|
||||
) {
|
||||
Ok(patch) => {
|
||||
let patch = patch.trim();
|
||||
let patch = radicle::cob::ObjectId::from_str(patch)?;
|
||||
|
||||
// Create long-lived patch head reference, now that we know the Patch ID.
|
||||
//
|
||||
// refs/namespaces/<nid>/refs/heads/patches/<patch-id>
|
||||
//
|
||||
let refname = git::refs::storage::patch(nid, &patch);
|
||||
let _ = stored.reference(
|
||||
refname.as_str(),
|
||||
commit.id(),
|
||||
true,
|
||||
"Create reference for patch head",
|
||||
)?;
|
||||
|
||||
let head = working.head()?;
|
||||
if head.peel_to_commit()?.id() == commit.id() {
|
||||
if let Ok(r) = head.resolve() {
|
||||
let branch = git::raw::Branch::wrap(r);
|
||||
let name: Option<git::RefString> =
|
||||
branch.name()?.and_then(|b| b.try_into().ok());
|
||||
|
||||
working.reference(
|
||||
&git::refs::workdir::patch_upstream(&patch),
|
||||
commit.id(),
|
||||
// The patch shouldn't exist yet, and so neither should
|
||||
// this ref.
|
||||
false,
|
||||
"Create remote tracking branch for patch",
|
||||
)?;
|
||||
|
||||
if let Some(name) = name {
|
||||
if branch.upstream().is_err() {
|
||||
git::set_upstream(
|
||||
working,
|
||||
&*radicle::rad::REMOTE_NAME,
|
||||
name.as_str(),
|
||||
git::refs::workdir::patch(&patch),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
// Delete short-lived patch head reference.
|
||||
stored.find_reference(dst).map(|mut r| r.delete()).ok();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Update an existing patch.
|
||||
fn patch_update(
|
||||
src: &git::RefStr,
|
||||
dst: &git::RefStr,
|
||||
force: bool,
|
||||
oid: &git::Oid,
|
||||
nid: &NodeId,
|
||||
working: &git::raw::Repository,
|
||||
stored: &git::raw::Repository,
|
||||
) -> Result<(), Error> {
|
||||
push_ref(src, dst, force, nid, working, stored)?;
|
||||
|
||||
// Patch ID.
|
||||
let oid = radicle::cob::ObjectId::from(oid);
|
||||
let stderr = io::stderr().as_raw_fd();
|
||||
|
||||
// Nb. Git tooling checks that we aren't attempting a forced update without the `--force`
|
||||
// option of `git push`. There's no need for us to check here.
|
||||
|
||||
execute(
|
||||
"rad",
|
||||
["patch", "update", "--quiet", "--no-push", &oid.to_string()],
|
||||
unsafe { process::Stdio::from_raw_fd(stderr) },
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Push a single reference to storage.
|
||||
fn push_ref(
|
||||
src: &git::RefStr,
|
||||
dst: &git::RefStr,
|
||||
force: bool,
|
||||
nid: &NodeId,
|
||||
working: &git::raw::Repository,
|
||||
stored: &git::raw::Repository,
|
||||
) -> Result<(), Error> {
|
||||
let mut remote = working.remote_anonymous(&git::url::File::new(stored.path()).to_string())?;
|
||||
let dst = nid.to_namespace().join(dst);
|
||||
let refspec = git::Refspec { src, dst, force };
|
||||
|
||||
// Nb. The *force* indicator (`+`) is processed by Git tooling before we even reach this code.
|
||||
// This happens during the `list for-push` phase.
|
||||
remote.push(&[refspec.to_string().as_str()], None)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute a command as a child process, redirecting its stdout to the given `Stdio`.
|
||||
fn execute<S: AsRef<std::ffi::OsStr>>(
|
||||
name: &str,
|
||||
args: impl IntoIterator<Item = S>,
|
||||
stdout: process::Stdio,
|
||||
) -> Result<String, Error> {
|
||||
let mut cmd = process::Command::new(name);
|
||||
cmd.args(args)
|
||||
.stdout(stdout)
|
||||
.stderr(process::Stdio::inherit());
|
||||
|
||||
let child = cmd.spawn()?;
|
||||
let output = child.wait_with_output()?;
|
||||
let status = output.status;
|
||||
|
||||
if !status.success() {
|
||||
let cmd = format!(
|
||||
"{} {}",
|
||||
cmd.get_program().to_string_lossy(),
|
||||
cmd.get_args()
|
||||
.collect::<Vec<_>>()
|
||||
.join(OsStr::new(" "))
|
||||
.to_string_lossy()
|
||||
);
|
||||
return Err(Error::CommandFailed(cmd, status.code().unwrap_or(-1)));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
use std::ffi::OsString;
|
||||
use std::io::Write;
|
||||
use std::os::fd::{AsRawFd, FromRawFd};
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::{env, fs, io};
|
||||
|
|
@ -66,7 +67,11 @@ impl Editor {
|
|||
)
|
||||
);
|
||||
};
|
||||
process::Command::new(cmd).arg(&self.path).spawn()?.wait()?;
|
||||
process::Command::new(cmd)
|
||||
.stdout(unsafe { process::Stdio::from_raw_fd(io::stderr().as_raw_fd()) })
|
||||
.arg(&self.path)
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
|
||||
let text = fs::read_to_string(&self.path)?;
|
||||
if text.trim().is_empty() {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ pub use git2 as raw;
|
|||
pub use git_ext::ref_format as fmt;
|
||||
pub use git_ext::ref_format::{
|
||||
component, lit, name, qualified, refname, refspec,
|
||||
refspec::{PatternStr, PatternString},
|
||||
refspec::{PatternStr, PatternString, Refspec},
|
||||
Component, Namespaced, Qualified, RefStr, RefString,
|
||||
};
|
||||
pub use radicle_git_ext as ext;
|
||||
|
|
@ -136,6 +136,7 @@ pub enum ListRefsError {
|
|||
|
||||
pub mod refs {
|
||||
use super::*;
|
||||
use radicle_cob as cob;
|
||||
|
||||
/// Try to get a qualified reference from a generic reference.
|
||||
pub fn qualified_from<'a>(r: &'a git2::Reference) -> Result<(Qualified<'a>, Oid), RefError> {
|
||||
|
|
@ -157,8 +158,6 @@ pub mod refs {
|
|||
refspec::{self, PatternString},
|
||||
};
|
||||
|
||||
use radicle_cob as cob;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Where the project's identity document is stored.
|
||||
|
|
@ -220,10 +219,46 @@ pub mod refs {
|
|||
.join(Component::from(typename))
|
||||
.join(Component::from(object_id))
|
||||
}
|
||||
|
||||
/// A patch reference.
|
||||
///
|
||||
/// `refs/namespaces/<remote>/refs/heads/patches/<object_id>`
|
||||
///
|
||||
pub fn patch<'a>(remote: &RemoteId, object_id: &cob::ObjectId) -> Namespaced<'a> {
|
||||
Qualified::from_components(
|
||||
component!("heads"),
|
||||
component!("patches"),
|
||||
Some(object_id.into()),
|
||||
)
|
||||
.with_namespace(remote.into())
|
||||
}
|
||||
|
||||
/// Staging/temporary references.
|
||||
pub mod staging {
|
||||
use super::*;
|
||||
|
||||
/// Where patches are pushed initially, when they don't have an object-id yet.
|
||||
/// This is a short-lived reference, which is deleted after the patch has been opened.
|
||||
/// The `<oid>` is the commit proposed in the patch.
|
||||
///
|
||||
/// `refs/namespaces/<remote>/refs/patch/heads/<oid>`
|
||||
///
|
||||
pub fn patch<'a>(remote: &RemoteId, oid: impl Into<Oid>) -> Namespaced<'a> {
|
||||
// SAFETY: OIDs are valid reference names and valid path component.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let oid = RefString::try_from(oid.into().to_string()).unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let oid = Component::from_refstr(oid).unwrap();
|
||||
|
||||
Qualified::from_components(component!("patch"), component!("heads"), Some(oid))
|
||||
.with_namespace(remote.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod workdir {
|
||||
use super::*;
|
||||
use format::name::component;
|
||||
|
||||
/// Create a [`RefString`] that corresponds to `refs/heads/<branch>`.
|
||||
pub fn branch(branch: &RefStr) -> RefString {
|
||||
|
|
@ -244,6 +279,30 @@ pub mod refs {
|
|||
pub fn tag(name: &RefStr) -> RefString {
|
||||
refname!("refs/tags").join(name)
|
||||
}
|
||||
|
||||
/// A patch head.
|
||||
///
|
||||
/// `refs/heads/patches/<patch-id>`
|
||||
///
|
||||
pub fn patch<'a>(patch_id: &cob::ObjectId) -> Qualified<'a> {
|
||||
Qualified::from_components(
|
||||
component!("heads"),
|
||||
component!("patches"),
|
||||
Some(patch_id.into()),
|
||||
)
|
||||
}
|
||||
|
||||
/// A patch head.
|
||||
///
|
||||
/// `refs/remotes/rad/patches/<patch-id>`
|
||||
///
|
||||
pub fn patch_upstream<'a>(patch_id: &cob::ObjectId) -> Qualified<'a> {
|
||||
Qualified::from_components(
|
||||
component!("remotes"),
|
||||
crate::rad::REMOTE_COMPONENT.clone(),
|
||||
[component!("patches"), patch_id.into()],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -365,12 +424,23 @@ pub fn write_tree<'r>(
|
|||
Ok(tree)
|
||||
}
|
||||
|
||||
/// Configure a radicle repository.
|
||||
///
|
||||
/// * Sets `push.default = upstream`.
|
||||
pub fn configure_repository(repo: &git2::Repository) -> Result<(), git2::Error> {
|
||||
let mut cfg = repo.config()?;
|
||||
cfg.set_str("push.default", "upstream")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configure a repository's radicle remote.
|
||||
///
|
||||
/// The entry for this remote will be:
|
||||
/// ```text
|
||||
/// [remote.<name>]
|
||||
/// url = <url>
|
||||
/// url = <fetch>
|
||||
/// pushurl = <push>
|
||||
/// fetch +refs/heads/*:refs/remotes/<name>/*
|
||||
/// ```
|
||||
pub fn configure_remote<'r>(
|
||||
|
|
@ -381,7 +451,36 @@ pub fn configure_remote<'r>(
|
|||
) -> Result<git2::Remote<'r>, git2::Error> {
|
||||
let fetchspec = format!("+refs/heads/*:refs/remotes/{name}/*");
|
||||
let remote = repo.remote_with_fetch(name, fetch.to_string().as_str(), &fetchspec)?;
|
||||
repo.remote_set_pushurl(name, Some(push.to_string().as_str()))?;
|
||||
|
||||
if push != fetch {
|
||||
repo.remote_set_pushurl(name, Some(push.to_string().as_str()))?;
|
||||
}
|
||||
Ok(remote)
|
||||
}
|
||||
|
||||
/// Configure a repository's patches remote.
|
||||
///
|
||||
/// The entry for this remote will be:
|
||||
/// ```text
|
||||
/// [remote.<name>]
|
||||
/// url = <url>
|
||||
/// pushurl = <url>
|
||||
/// push HEAD:refs/patches
|
||||
/// ```
|
||||
pub fn configure_patches_remote<'r>(
|
||||
repo: &'r git2::Repository,
|
||||
name: &str,
|
||||
refspec: &Refspec<RefString, RefString>,
|
||||
push: &Url,
|
||||
) -> Result<git2::Remote<'r>, git2::Error> {
|
||||
let push = push.to_string();
|
||||
let remote = repo.remote(name, &push)?;
|
||||
|
||||
// This fetchspec basically prevents fetching from this remote,
|
||||
// as it shouldn't be used for fetching.
|
||||
repo.remote_add_fetch(name, "refs/patches:refs/patches")?;
|
||||
repo.remote_set_pushurl(name, Some(&push))?;
|
||||
repo.remote_add_push(name, refspec.to_string().as_str())?;
|
||||
|
||||
Ok(remote)
|
||||
}
|
||||
|
|
@ -428,10 +527,14 @@ pub fn push<'a>(
|
|||
/// ```
|
||||
pub fn set_upstream(
|
||||
repo: &git2::Repository,
|
||||
remote: &str,
|
||||
branch: &str,
|
||||
merge: &str,
|
||||
remote: impl AsRef<str>,
|
||||
branch: impl AsRef<str>,
|
||||
merge: impl AsRef<str>,
|
||||
) -> Result<(), git2::Error> {
|
||||
let remote = remote.as_ref();
|
||||
let branch = branch.as_ref();
|
||||
let merge = merge.as_ref();
|
||||
|
||||
let mut config = repo.config()?;
|
||||
let branch_remote = format!("branch.{branch}.remote");
|
||||
let branch_merge = format!("branch.{branch}.merge");
|
||||
|
|
@ -500,8 +603,8 @@ pub mod url {
|
|||
|
||||
impl File {
|
||||
/// Create a new file URL pointing to the given path.
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||
Self { path: path.into() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -515,8 +618,7 @@ pub mod url {
|
|||
/// Git environment variables.
|
||||
pub mod env {
|
||||
/// Set of environment vars to reset git's configuration to default.
|
||||
pub const GIT_DEFAULT_CONFIG: [(&str, &str); 3] = [
|
||||
("GIT_CONFIG", "/dev/null"),
|
||||
pub const GIT_DEFAULT_CONFIG: [(&str, &str); 2] = [
|
||||
("GIT_CONFIG_GLOBAL", "/dev/null"),
|
||||
("GIT_CONFIG_NOSYSTEM", "1"),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ use crate::{identity, storage};
|
|||
|
||||
/// Name of the radicle storage remote.
|
||||
pub static REMOTE_NAME: Lazy<git::RefString> = Lazy::new(|| git::refname!("rad"));
|
||||
/// Name of the radicle storage remote.
|
||||
pub static REMOTE_COMPONENT: Lazy<git::Component> = Lazy::new(|| git::fmt::name::component!("rad"));
|
||||
/// Name of the radicle patches remote.
|
||||
pub static PATCHES_REMOTE_NAME: Lazy<git::RefString> = Lazy::new(|| git::refname!("rad/patches"));
|
||||
/// Refname used for pushing patches.
|
||||
pub static PATCHES_REFNAME: Lazy<git::RefString> = Lazy::new(|| git::refname!("refs/patches"));
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum InitError {
|
||||
|
|
@ -72,7 +78,19 @@ pub fn init<G: Signer, S: WriteStorage>(
|
|||
let (project, _) = Repository::init(&doc, pk, storage, signer)?;
|
||||
let url = git::Url::from(project.id);
|
||||
|
||||
git::configure_repository(repo)?;
|
||||
git::configure_remote(repo, &REMOTE_NAME, &url, &url.clone().with_namespace(*pk))?;
|
||||
git::configure_patches_remote(
|
||||
repo,
|
||||
&PATCHES_REMOTE_NAME,
|
||||
&git::Refspec {
|
||||
src: git::refname!("HEAD"),
|
||||
dst: PATCHES_REFNAME.clone(),
|
||||
force: false,
|
||||
},
|
||||
&url.with_namespace(*pk),
|
||||
)?;
|
||||
|
||||
git::push(
|
||||
repo,
|
||||
&REMOTE_NAME,
|
||||
|
|
@ -249,7 +267,7 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
|
|||
repo.checkout_head(None)?;
|
||||
|
||||
// Setup remote tracking for default branch.
|
||||
git::set_upstream(&repo, &REMOTE_NAME, project.default_branch(), branch_ref)?;
|
||||
git::set_upstream(&repo, &*REMOTE_NAME, project.default_branch(), branch_ref)?;
|
||||
}
|
||||
|
||||
Ok(repo)
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@ pub trait ReadRepository {
|
|||
/// Get the [`git2::Commit`] found using its `oid`.
|
||||
///
|
||||
/// Returns `None` if the commit did not exist.
|
||||
fn commit(&self, oid: Oid) -> Result<git2::Commit, git_ext::Error>;
|
||||
fn commit(&self, oid: Oid) -> Result<git2::Commit, git::ext::Error>;
|
||||
|
||||
/// Perform a revision walk of a commit history starting from the given head.
|
||||
fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
|
||||
|
|
@ -400,6 +400,16 @@ pub trait ReadRepository {
|
|||
/// Get all references of the given remote.
|
||||
fn references_of(&self, remote: &RemoteId) -> Result<Refs, Error>;
|
||||
|
||||
/// Get all references following a pattern.
|
||||
/// Skips references with names that are not parseable into [`Qualified`].
|
||||
///
|
||||
/// This function always peels reference to the commit. For tags, this means the [`Oid`] of the
|
||||
/// commit pointed to by the tag is returned, and not the [`Oid`] of the tag itsself.
|
||||
fn references_glob(
|
||||
&self,
|
||||
pattern: &git::PatternStr,
|
||||
) -> Result<Vec<(Qualified, Oid)>, git::ext::Error>;
|
||||
|
||||
/// Get the given remote.
|
||||
fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>;
|
||||
|
||||
|
|
|
|||
|
|
@ -457,6 +457,27 @@ impl ReadRepository for Repository {
|
|||
Ok(refs.into())
|
||||
}
|
||||
|
||||
fn references_glob(
|
||||
&self,
|
||||
pattern: &self::PatternStr,
|
||||
) -> Result<Vec<(Qualified, Oid)>, self::ext::Error> {
|
||||
let mut refs = Vec::new();
|
||||
|
||||
for r in self.backend.references_glob(pattern)? {
|
||||
let r = r?;
|
||||
let c = r.peel_to_commit()?;
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
Ok(refs)
|
||||
}
|
||||
|
||||
fn remotes(&self) -> Result<Remotes<Verified>, refs::Error> {
|
||||
let mut remotes = Vec::new();
|
||||
for remote in Repository::remotes(self)? {
|
||||
|
|
|
|||
|
|
@ -201,6 +201,13 @@ impl ReadRepository for MockRepository {
|
|||
todo!()
|
||||
}
|
||||
|
||||
fn references_glob(
|
||||
&self,
|
||||
_pattern: &git::PatternStr,
|
||||
) -> Result<Vec<(fmt::Qualified, Oid)>, git::ext::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn identity_doc(
|
||||
&self,
|
||||
) -> Result<(Oid, crate::identity::Doc<crate::crypto::Unverified>), IdentityError> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue