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 = [
|
dependencies = [
|
||||||
"radicle",
|
"radicle",
|
||||||
"radicle-crypto",
|
"radicle-crypto",
|
||||||
|
"radicle-git-ext",
|
||||||
"thiserror",
|
"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
|
$ rad remote list
|
||||||
bob z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (fetch)
|
bob z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (fetch)
|
||||||
rad the canonical upstream (fetch)
|
rad (canonical upstream) (fetch)
|
||||||
rad z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
|
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.
|
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.
|
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
|
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
|
||||||
✓ Remote bob added with rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
✓ Remote bob added with rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
|
```
|
||||||
|
|
||||||
|
``` (stderr)
|
||||||
$ git fetch bob
|
$ 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.
|
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`
|
// Setup eg. `master` -> `rad/master`
|
||||||
radicle::git::set_upstream(
|
radicle::git::set_upstream(
|
||||||
&repo,
|
&repo,
|
||||||
&radicle::rad::REMOTE_NAME,
|
&*radicle::rad::REMOTE_NAME,
|
||||||
proj.default_branch(),
|
proj.default_branch(),
|
||||||
&radicle::git::refs::workdir::branch(proj.default_branch()),
|
radicle::git::refs::workdir::branch(proj.default_branch()),
|
||||||
)?;
|
)?;
|
||||||
} else {
|
} else {
|
||||||
push_cmd = format!("git push {}", *radicle::rad::REMOTE_NAME);
|
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)?;
|
let (target_ref, target_oid) = get_merge_target(storage, &head_branch)?;
|
||||||
|
|
||||||
if head_branch.upstream().is_err() {
|
if head_branch.upstream().is_err() {
|
||||||
|
// TODO(cloudhead): Disable this when invoked from remote helper.
|
||||||
radicle::git::set_upstream(
|
radicle::git::set_upstream(
|
||||||
workdir,
|
workdir,
|
||||||
&radicle::rad::REMOTE_NAME,
|
&*radicle::rad::REMOTE_NAME,
|
||||||
branch_name(&head_branch)?,
|
branch_name(&head_branch)?,
|
||||||
&head_branch_name,
|
head_branch_name,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use radicle::rad;
|
||||||
use radicle_term::{Element, Table};
|
use radicle_term::{Element, Table};
|
||||||
|
|
||||||
use crate::git;
|
use crate::git;
|
||||||
|
|
@ -11,13 +12,22 @@ pub fn run(repo: &git::Repository) -> anyhow::Result<()> {
|
||||||
let Some(url) = url else {
|
let Some(url) = url else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let nid = url.namespace.map_or(
|
|
||||||
term::format::dim("the canonical upstream".to_string()).italic(),
|
let description = if r.name == rad::PATCHES_REMOTE_NAME.as_str() {
|
||||||
|namespace| term::format::tertiary(namespace.to_string()),
|
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([
|
table.push([
|
||||||
term::format::bold(r.name.clone()),
|
term::format::bold(r.name.clone()),
|
||||||
nid,
|
description,
|
||||||
term::format::parens(term::format::secondary(dir.to_owned())),
|
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<_>>();
|
let mut seeds = seeds.connected().collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
if seeds.is_empty() {
|
if seeds.is_empty() {
|
||||||
term::info!("Not connected to any seeds");
|
term::info!("Not connected to any seeds.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
node.announce_refs(rid)?;
|
node.announce_refs(rid)?;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ impl<'a> SetupRemote<'a> {
|
||||||
let local_branch = radicle::git::refs::workdir::branch(
|
let local_branch = radicle::git::refs::workdir::branch(
|
||||||
node_ref.join(&self.default_branch).as_refstr(),
|
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)));
|
return Ok(Some((remote, branch_name)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,28 @@ fn rad_patch_draft() {
|
||||||
.unwrap();
|
.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]
|
#[test]
|
||||||
fn rad_rm() {
|
fn rad_rm() {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
|
|
@ -791,6 +813,60 @@ fn rad_remote() {
|
||||||
.unwrap();
|
.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]
|
#[test]
|
||||||
fn rad_workflow() {
|
fn rad_workflow() {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
radicle-git-ext = { version = "0" }
|
||||||
|
|
||||||
[dependencies.radicle]
|
[dependencies.radicle]
|
||||||
path = "../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)]
|
//! The Radicle Git remote helper.
|
||||||
use std::os::fd::{AsRawFd, FromRawFd};
|
//!
|
||||||
|
//! 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::path::PathBuf;
|
||||||
use std::{env, io, process};
|
use std::{env, io};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use radicle::crypto::PublicKey;
|
use radicle::git;
|
||||||
use radicle::node::Handle;
|
|
||||||
use radicle::storage::git::transport::local::{Url, UrlError};
|
use radicle::storage::git::transport::local::{Url, UrlError};
|
||||||
use radicle::storage::{ReadRepository, WriteRepository, WriteStorage};
|
use radicle::storage::{ReadRepository, 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";
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// Remote repository not found (or empty).
|
/// Remote repository not found (or empty).
|
||||||
#[error("remote repository `{0}` not found")]
|
#[error("remote repository `{0}` not found")]
|
||||||
RepositoryNotFound(PathBuf),
|
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.
|
/// Invalid command received.
|
||||||
#[error("invalid command `{0}`")]
|
#[error("invalid command `{0}`")]
|
||||||
InvalidCommand(String),
|
InvalidCommand(String),
|
||||||
|
|
@ -38,12 +29,31 @@ pub enum Error {
|
||||||
/// Error with the remote url.
|
/// Error with the remote url.
|
||||||
#[error("invalid remote url: {0}")]
|
#[error("invalid remote url: {0}")]
|
||||||
RemoteUrl(#[from] UrlError),
|
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.
|
/// Run the radicle remote helper using the given profile.
|
||||||
pub fn run(profile: radicle::Profile) -> Result<(), Box<dyn std::error::Error + 'static>> {
|
pub fn run(profile: radicle::Profile) -> Result<(), Error> {
|
||||||
// `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)?;
|
|
||||||
let url: Url = {
|
let url: Url = {
|
||||||
let args = env::args().skip(1).take(2).collect::<Vec<_>>();
|
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(),
|
[_, url] => url.parse(),
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Error::InvalidArguments(args).into());
|
return Err(Error::InvalidArguments(args));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let proj = profile.storage.repository_mut(url.repo)?;
|
let stored = profile.storage.repository_mut(url.repo)?;
|
||||||
if proj.is_empty()? {
|
if stored.is_empty()? {
|
||||||
return Err(Error::RepositoryNotFound(proj.path().to_path_buf()).into());
|
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();
|
let stdin = io::stdin();
|
||||||
loop {
|
let mut line = String::new();
|
||||||
let mut line = String::new();
|
|
||||||
let read = stdin.read_line(&mut line)?;
|
loop {
|
||||||
if read == 0 {
|
let tokens = read_line(&stdin, &mut line)?;
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tokens = line.trim().split(' ').collect::<Vec<_>>();
|
|
||||||
match tokens.as_slice() {
|
match tokens.as_slice() {
|
||||||
// First we are asked about capabilities.
|
|
||||||
["capabilities"] => {
|
["capabilities"] => {
|
||||||
println!("connect");
|
println!("option");
|
||||||
|
println!("push"); // Implies `list` command.
|
||||||
|
println!("fetch");
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
// Since we send a `connect` back, this is what is requested next.
|
["option", "verbosity"] => {
|
||||||
["connect", service] => {
|
println!("ok");
|
||||||
// Don't allow push if either of these conditions is true:
|
}
|
||||||
//
|
["option", "push-option", _opt] => {
|
||||||
// 1. Our key is not in ssh-agent, which means we won't be able to sign the refs.
|
println!("unsupported");
|
||||||
// 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.
|
["option", "progress", ..] => {
|
||||||
// 3. The URL namespace is not set, which is used for fetching canonical refs.
|
println!("unsupported");
|
||||||
let signer = if *service == GIT_RECEIVE_PACK {
|
}
|
||||||
match url.namespace {
|
["option", ..] => {
|
||||||
Some(namespace) => {
|
println!("unsupported");
|
||||||
if profile.public_key != namespace {
|
}
|
||||||
return Err(Error::KeyMismatch(profile.public_key).into());
|
["fetch", _oid, refstr] => {
|
||||||
}
|
return fetch::run(vec![refstr.to_string()], &working, url, stored, &stdin)
|
||||||
}
|
.map_err(Error::from);
|
||||||
None => return Err(Error::NoKey.into()),
|
}
|
||||||
}
|
["push", refspec] => {
|
||||||
|
return push::run(
|
||||||
let signer = profile.signer()?;
|
vec![refspec.to_string()],
|
||||||
|
&working,
|
||||||
Some(signer)
|
url,
|
||||||
} else {
|
stored,
|
||||||
None
|
&profile,
|
||||||
};
|
&stdin,
|
||||||
|
)
|
||||||
if *service == GIT_UPLOAD_PACK {
|
.map_err(Error::from);
|
||||||
// TODO: Fetch from network.
|
}
|
||||||
}
|
["list"] => {
|
||||||
println!(); // Empty line signifies connection is established.
|
list::for_fetch(&url, &stored)?;
|
||||||
|
}
|
||||||
let mut child = process::Command::new(service)
|
["list", "for-push"] => {
|
||||||
.arg(proj.path())
|
list::for_push(&profile, &stored)?;
|
||||||
.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()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 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::ffi::OsString;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::os::fd::{AsRawFd, FromRawFd};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::{env, fs, io};
|
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)?;
|
let text = fs::read_to_string(&self.path)?;
|
||||||
if text.trim().is_empty() {
|
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 as fmt;
|
||||||
pub use git_ext::ref_format::{
|
pub use git_ext::ref_format::{
|
||||||
component, lit, name, qualified, refname, refspec,
|
component, lit, name, qualified, refname, refspec,
|
||||||
refspec::{PatternStr, PatternString},
|
refspec::{PatternStr, PatternString, Refspec},
|
||||||
Component, Namespaced, Qualified, RefStr, RefString,
|
Component, Namespaced, Qualified, RefStr, RefString,
|
||||||
};
|
};
|
||||||
pub use radicle_git_ext as ext;
|
pub use radicle_git_ext as ext;
|
||||||
|
|
@ -136,6 +136,7 @@ pub enum ListRefsError {
|
||||||
|
|
||||||
pub mod refs {
|
pub mod refs {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use radicle_cob as cob;
|
||||||
|
|
||||||
/// Try to get a qualified reference from a generic reference.
|
/// Try to get a qualified reference from a generic reference.
|
||||||
pub fn qualified_from<'a>(r: &'a git2::Reference) -> Result<(Qualified<'a>, Oid), RefError> {
|
pub fn qualified_from<'a>(r: &'a git2::Reference) -> Result<(Qualified<'a>, Oid), RefError> {
|
||||||
|
|
@ -157,8 +158,6 @@ pub mod refs {
|
||||||
refspec::{self, PatternString},
|
refspec::{self, PatternString},
|
||||||
};
|
};
|
||||||
|
|
||||||
use radicle_cob as cob;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Where the project's identity document is stored.
|
/// Where the project's identity document is stored.
|
||||||
|
|
@ -220,10 +219,46 @@ pub mod refs {
|
||||||
.join(Component::from(typename))
|
.join(Component::from(typename))
|
||||||
.join(Component::from(object_id))
|
.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 {
|
pub mod workdir {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use format::name::component;
|
||||||
|
|
||||||
/// Create a [`RefString`] that corresponds to `refs/heads/<branch>`.
|
/// Create a [`RefString`] that corresponds to `refs/heads/<branch>`.
|
||||||
pub fn branch(branch: &RefStr) -> RefString {
|
pub fn branch(branch: &RefStr) -> RefString {
|
||||||
|
|
@ -244,6 +279,30 @@ pub mod refs {
|
||||||
pub fn tag(name: &RefStr) -> RefString {
|
pub fn tag(name: &RefStr) -> RefString {
|
||||||
refname!("refs/tags").join(name)
|
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)
|
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.
|
/// Configure a repository's radicle remote.
|
||||||
///
|
///
|
||||||
/// The entry for this remote will be:
|
/// The entry for this remote will be:
|
||||||
/// ```text
|
/// ```text
|
||||||
/// [remote.<name>]
|
/// [remote.<name>]
|
||||||
/// url = <url>
|
/// url = <fetch>
|
||||||
|
/// pushurl = <push>
|
||||||
/// fetch +refs/heads/*:refs/remotes/<name>/*
|
/// fetch +refs/heads/*:refs/remotes/<name>/*
|
||||||
/// ```
|
/// ```
|
||||||
pub fn configure_remote<'r>(
|
pub fn configure_remote<'r>(
|
||||||
|
|
@ -381,7 +451,36 @@ pub fn configure_remote<'r>(
|
||||||
) -> Result<git2::Remote<'r>, git2::Error> {
|
) -> Result<git2::Remote<'r>, git2::Error> {
|
||||||
let fetchspec = format!("+refs/heads/*:refs/remotes/{name}/*");
|
let fetchspec = format!("+refs/heads/*:refs/remotes/{name}/*");
|
||||||
let remote = repo.remote_with_fetch(name, fetch.to_string().as_str(), &fetchspec)?;
|
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)
|
Ok(remote)
|
||||||
}
|
}
|
||||||
|
|
@ -428,10 +527,14 @@ pub fn push<'a>(
|
||||||
/// ```
|
/// ```
|
||||||
pub fn set_upstream(
|
pub fn set_upstream(
|
||||||
repo: &git2::Repository,
|
repo: &git2::Repository,
|
||||||
remote: &str,
|
remote: impl AsRef<str>,
|
||||||
branch: &str,
|
branch: impl AsRef<str>,
|
||||||
merge: &str,
|
merge: impl AsRef<str>,
|
||||||
) -> Result<(), git2::Error> {
|
) -> Result<(), git2::Error> {
|
||||||
|
let remote = remote.as_ref();
|
||||||
|
let branch = branch.as_ref();
|
||||||
|
let merge = merge.as_ref();
|
||||||
|
|
||||||
let mut config = repo.config()?;
|
let mut config = repo.config()?;
|
||||||
let branch_remote = format!("branch.{branch}.remote");
|
let branch_remote = format!("branch.{branch}.remote");
|
||||||
let branch_merge = format!("branch.{branch}.merge");
|
let branch_merge = format!("branch.{branch}.merge");
|
||||||
|
|
@ -500,8 +603,8 @@ pub mod url {
|
||||||
|
|
||||||
impl File {
|
impl File {
|
||||||
/// Create a new file URL pointing to the given path.
|
/// Create a new file URL pointing to the given path.
|
||||||
pub fn new(path: PathBuf) -> Self {
|
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||||
Self { path }
|
Self { path: path.into() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -515,8 +618,7 @@ pub mod url {
|
||||||
/// Git environment variables.
|
/// Git environment variables.
|
||||||
pub mod env {
|
pub mod env {
|
||||||
/// Set of environment vars to reset git's configuration to default.
|
/// Set of environment vars to reset git's configuration to default.
|
||||||
pub const GIT_DEFAULT_CONFIG: [(&str, &str); 3] = [
|
pub const GIT_DEFAULT_CONFIG: [(&str, &str); 2] = [
|
||||||
("GIT_CONFIG", "/dev/null"),
|
|
||||||
("GIT_CONFIG_GLOBAL", "/dev/null"),
|
("GIT_CONFIG_GLOBAL", "/dev/null"),
|
||||||
("GIT_CONFIG_NOSYSTEM", "1"),
|
("GIT_CONFIG_NOSYSTEM", "1"),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,12 @@ use crate::{identity, storage};
|
||||||
|
|
||||||
/// Name of the radicle storage remote.
|
/// Name of the radicle storage remote.
|
||||||
pub static REMOTE_NAME: Lazy<git::RefString> = Lazy::new(|| git::refname!("rad"));
|
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)]
|
#[derive(Error, Debug)]
|
||||||
pub enum InitError {
|
pub enum InitError {
|
||||||
|
|
@ -72,7 +78,19 @@ pub fn init<G: Signer, S: WriteStorage>(
|
||||||
let (project, _) = Repository::init(&doc, pk, storage, signer)?;
|
let (project, _) = Repository::init(&doc, pk, storage, signer)?;
|
||||||
let url = git::Url::from(project.id);
|
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_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(
|
git::push(
|
||||||
repo,
|
repo,
|
||||||
&REMOTE_NAME,
|
&REMOTE_NAME,
|
||||||
|
|
@ -249,7 +267,7 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
|
||||||
repo.checkout_head(None)?;
|
repo.checkout_head(None)?;
|
||||||
|
|
||||||
// Setup remote tracking for default branch.
|
// 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)
|
Ok(repo)
|
||||||
|
|
|
||||||
|
|
@ -385,7 +385,7 @@ pub trait ReadRepository {
|
||||||
/// Get the [`git2::Commit`] found using its `oid`.
|
/// Get the [`git2::Commit`] found using its `oid`.
|
||||||
///
|
///
|
||||||
/// Returns `None` if the commit did not exist.
|
/// 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.
|
/// Perform a revision walk of a commit history starting from the given head.
|
||||||
fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
|
fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
|
||||||
|
|
@ -400,6 +400,16 @@ pub trait ReadRepository {
|
||||||
/// Get all references of the given remote.
|
/// Get all references of the given remote.
|
||||||
fn references_of(&self, remote: &RemoteId) -> Result<Refs, Error>;
|
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.
|
/// Get the given remote.
|
||||||
fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>;
|
fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -457,6 +457,27 @@ impl ReadRepository for Repository {
|
||||||
Ok(refs.into())
|
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> {
|
fn remotes(&self) -> Result<Remotes<Verified>, refs::Error> {
|
||||||
let mut remotes = Vec::new();
|
let mut remotes = Vec::new();
|
||||||
for remote in Repository::remotes(self)? {
|
for remote in Repository::remotes(self)? {
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,13 @@ impl ReadRepository for MockRepository {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn references_glob(
|
||||||
|
&self,
|
||||||
|
_pattern: &git::PatternStr,
|
||||||
|
) -> Result<Vec<(fmt::Qualified, Oid)>, git::ext::Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
fn identity_doc(
|
fn identity_doc(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<(Oid, crate::identity::Doc<crate::crypto::Unverified>), IdentityError> {
|
) -> Result<(Oid, crate::identity::Doc<crate::crypto::Unverified>), IdentityError> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue