mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-13 18:40:50 +08:00
Migrate to the heartwood protocol stack
Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
@@ -14,8 +14,8 @@ jobs:
|
||||
matrix:
|
||||
# Disable firefox until this is fixed upstream:
|
||||
# https://github.com/microsoft/playwright/issues/21145
|
||||
# browser: [chromium, firefox, visual, heartwood]
|
||||
browser: [chromium, visual, heartwood]
|
||||
# browser: [chromium, firefox, visual]
|
||||
browser: [chromium, visual]
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -59,19 +59,13 @@ jobs:
|
||||
|
||||
- name: Start http-api test server
|
||||
run: |
|
||||
if [ ${{ matrix.browser }} = "heartwood" ]; then
|
||||
mkdir -p tests/artifacts
|
||||
./scripts/run-httpd-with-fixtures --non-interactive --download 2>&1 | tee tests/artifacts/httpd-${{ matrix.browser }}.log &
|
||||
else
|
||||
./scripts/run-http-api-with-fixtures --non-interactive --detach
|
||||
fi
|
||||
mkdir -p tests/artifacts;
|
||||
./scripts/run-httpd-with-fixtures --non-interactive --download 2>&1 | tee tests/artifacts/httpd-${{ matrix.browser }}.log &
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
if [ ${{ matrix.browser }} = "visual" ] && [ ${{ github.ref }} = "refs/heads/master" ]; then
|
||||
npm run test:e2e -- --project ${{ matrix.browser }} --update-snapshots;
|
||||
elif [ ${{ matrix.browser }} = "heartwood" ]; then
|
||||
HEARTWOOD=true npm run test:e2e -- --project chromium;
|
||||
else
|
||||
npm run test:e2e -- --project ${{ matrix.browser }};
|
||||
fi
|
||||
|
||||
+71
-45
@@ -1,30 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
killall radicle-node
|
||||
killall git-daemon
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
function cleanup {
|
||||
docker kill radicle-git-server-test
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
|
||||
PASSPHRASE=asdf
|
||||
REV=40bdc662b2d48cbfaa87182b21457ef8f861b04a
|
||||
export RAD_PASSPHRASE=asdf
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
ID=$(echo $RANDOM | md5sum | head -c 8)
|
||||
BASE_PATH=$REPO_ROOT/tests/tmp/create-seed-fixture-$ID
|
||||
ID=$(echo $RANDOM | md5sum | head -c 4)
|
||||
BASE_PATH=$REPO_ROOT/tests/tmp/seed-fixture-$ID
|
||||
|
||||
TEST_REPO_ARCHIVE=$REPO_ROOT/tests/fixtures/repos/source-browsing.tar.bz2
|
||||
TEST_REPO_NAME=source-browsing
|
||||
TEST_REPO_PATH=$BASE_PATH/repos/$TEST_REPO_NAME
|
||||
|
||||
PALM_RAD_HOME=$BASE_PATH/seeds/palm
|
||||
PALM_CHECKOUT=$BASE_PATH/checkout/palm
|
||||
ALICE_RAD_HOME=$BASE_PATH/peers/alice
|
||||
ALICE_CHECKOUT=$BASE_PATH/checkout/alice
|
||||
BOB_RAD_HOME=$BASE_PATH/peers/bob
|
||||
BOB_CHECKOUT=$BASE_PATH/checkout/bob
|
||||
|
||||
mkdir -p $PALM_RAD_HOME
|
||||
mkdir -p $PALM_CHECKOUT
|
||||
mkdir -p $ALICE_RAD_HOME
|
||||
mkdir -p $ALICE_CHECKOUT
|
||||
mkdir -p $BOB_RAD_HOME
|
||||
@@ -33,62 +32,89 @@ mkdir -p $TEST_REPO_PATH
|
||||
|
||||
tar -xf $TEST_REPO_ARCHIVE -C $TEST_REPO_PATH
|
||||
|
||||
RAD_HOME=$PALM_RAD_HOME rad auth --init --name palm --passphrase $PASSPHRASE
|
||||
### PALM NODE ###
|
||||
|
||||
docker run \
|
||||
--detach \
|
||||
--init \
|
||||
--publish 8778:8778 \
|
||||
--rm \
|
||||
--env RAD_HOME=/app/radicle \
|
||||
--name radicle-git-server-test \
|
||||
--volume $PALM_RAD_HOME:/app/radicle \
|
||||
"gcr.io/radicle-services/git-server:$REV" \
|
||||
--passphrase $PASSPHRASE \
|
||||
--allow-unauthorized-keys
|
||||
eval $(ssh-agent)
|
||||
|
||||
# git-server takes a while to copy commit hooks to the monorepo
|
||||
sleep 10
|
||||
export RAD_HOME=$PALM_RAD_HOME
|
||||
export RAD_SEED=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee
|
||||
|
||||
GIT_AUTHOR_NAME="Alice Liddell"
|
||||
GIT_AUTHOR_EMAIL="alice@radicle.xyz"
|
||||
GIT_COMMITTER_NAME="Alice Liddell"
|
||||
GIT_COMMITTER_EMAIL="alice@radicle.xyz"
|
||||
rad auth
|
||||
radicle-node --listen 0.0.0.0:3446 --git-daemon 0.0.0.0:4446 &
|
||||
|
||||
RAD_HOME=$ALICE_RAD_HOME rad auth --init --name alice --passphrase $PASSPHRASE
|
||||
### ALICE ###
|
||||
|
||||
export RAD_HOME=$ALICE_RAD_HOME
|
||||
export RAD_SEED=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
|
||||
|
||||
export GIT_AUTHOR_NAME="Alice Liddell"
|
||||
export GIT_AUTHOR_EMAIL="alice@radicle.xyz"
|
||||
export GIT_COMMITTER_NAME="Alice Liddell"
|
||||
export GIT_COMMITTER_EMAIL="alice@radicle.xyz"
|
||||
|
||||
rad auth
|
||||
|
||||
cd $ALICE_CHECKOUT
|
||||
|
||||
git clone $TEST_REPO_PATH
|
||||
cd $TEST_REPO_NAME
|
||||
git checkout feature/branch
|
||||
git checkout orphaned-branch
|
||||
|
||||
git checkout main
|
||||
rad init --name "source-browsing" \
|
||||
--description "Git repository for source browsing tests" \
|
||||
--default-branch "main" \
|
||||
--no-confirm
|
||||
|
||||
RAD_HOME=$ALICE_RAD_HOME rad init --name "source-browsing" --description "Git repository for source browsing tests" --default-branch "main" --no-confirm
|
||||
git checkout feature/branch
|
||||
git push rad
|
||||
|
||||
sleep 10
|
||||
RAD_HOME=$ALICE_RAD_HOME rad push --seed 0.0.0.0:8778 --all --sync
|
||||
PROJECT_ID=$(rad .)
|
||||
git checkout orphaned-branch
|
||||
git push rad
|
||||
|
||||
radicle-node --listen 0.0.0.0:3444 --git-daemon 0.0.0.0:4444 \
|
||||
--connect z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8@0.0.0.0:3446 &
|
||||
|
||||
GIT_AUTHOR_NAME="Bob Belcher"
|
||||
GIT_AUTHOR_EMAIL="bob@radicle.xyz"
|
||||
GIT_COMMITTER_NAME="Bob Belcher"
|
||||
GIT_COMMITTER_EMAIL="bob@radicle.xyz"
|
||||
sleep 1
|
||||
|
||||
RAD_HOME=$BOB_RAD_HOME rad auth --init --name bob --passphrase $PASSPHRASE
|
||||
### PALM CLONE ###
|
||||
|
||||
cd $PALM_CHECKOUT
|
||||
export RAD_HOME=$PALM_RAD_HOME
|
||||
rad clone rad:zKtT7DmF9H34KkvcKj9PHW19WzjT
|
||||
|
||||
### BOB ###
|
||||
|
||||
export GIT_AUTHOR_NAME="Bob Belcher"
|
||||
export GIT_AUTHOR_EMAIL="bob@radicle.xyz"
|
||||
export GIT_COMMITTER_NAME="Bob Belcher"
|
||||
export GIT_COMMITTER_EMAIL="bob@radicle.xyz"
|
||||
export GIT_COMMITTER_DATE="Mon Dec 21 14:00 2022 +0100"
|
||||
|
||||
export RAD_HOME=$BOB_RAD_HOME
|
||||
export RAD_SEED=fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe
|
||||
|
||||
rad auth
|
||||
rad auth
|
||||
radicle-node --listen 0.0.0.0:3445 --git-daemon 0.0.0.0:4445 \
|
||||
--connect z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8@0.0.0.0:3446 &
|
||||
|
||||
sleep 2
|
||||
|
||||
cd $BOB_CHECKOUT
|
||||
RAD_HOME=$BOB_RAD_HOME rad clone $PROJECT_ID --seed 0.0.0.0:8778 --no-confirm
|
||||
rad clone rad:zKtT7DmF9H34KkvcKj9PHW19WzjT
|
||||
|
||||
cd $TEST_REPO_NAME
|
||||
echo "Updated readme" > README.md
|
||||
git add README.md
|
||||
git commit --message "Update readme" --date "Mon Nov 21 14:00 2022 +0100"
|
||||
RAD_HOME=$BOB_RAD_HOME rad push --seed 0.0.0.0:8778
|
||||
RAD_HOME=$BOB_RAD_HOME rad sync --seed 0.0.0.0:8778 --self
|
||||
RAD_HOME=$BOB_RAD_HOME rad sync --seed 0.0.0.0:8778
|
||||
git commit --message "Update readme" --date "$GIT_COMMITTER_DATE"
|
||||
git push rad
|
||||
|
||||
### WAIT FOR SYNC WITH PALM ###
|
||||
|
||||
sleep 2
|
||||
|
||||
cd $BASE_PATH
|
||||
tar -cjf palm.tar.bz2 --exclude "post-receive" --exclude "pre-receive" -C $PALM_RAD_HOME .
|
||||
|
||||
killall radicle-node
|
||||
killall git-daemon
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
killall radicle-node
|
||||
killall git-daemon
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export RAD_PASSPHRASE=asdf
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
ID=$(echo $RANDOM | md5sum | head -c 4)
|
||||
BASE_PATH=$REPO_ROOT/tests/tmp/heartwood-$ID
|
||||
|
||||
TEST_REPO_ARCHIVE=$REPO_ROOT/tests/fixtures/repos/source-browsing.tar.bz2
|
||||
TEST_REPO_NAME=source-browsing
|
||||
TEST_REPO_PATH=$BASE_PATH/repos/$TEST_REPO_NAME
|
||||
|
||||
PALM_RAD_HOME=$BASE_PATH/seeds/palm
|
||||
PALM_CHECKOUT=$BASE_PATH/checkout/palm
|
||||
ALICE_RAD_HOME=$BASE_PATH/peers/alice
|
||||
ALICE_CHECKOUT=$BASE_PATH/checkout/alice
|
||||
BOB_RAD_HOME=$BASE_PATH/peers/bob
|
||||
BOB_CHECKOUT=$BASE_PATH/checkout/bob
|
||||
|
||||
mkdir -p $PALM_RAD_HOME
|
||||
mkdir -p $PALM_CHECKOUT
|
||||
mkdir -p $ALICE_RAD_HOME
|
||||
mkdir -p $ALICE_CHECKOUT
|
||||
mkdir -p $BOB_RAD_HOME
|
||||
mkdir -p $BOB_CHECKOUT
|
||||
mkdir -p $TEST_REPO_PATH
|
||||
|
||||
tar -xf $TEST_REPO_ARCHIVE -C $TEST_REPO_PATH
|
||||
|
||||
### PALM NODE ###
|
||||
|
||||
eval $(ssh-agent)
|
||||
|
||||
export RAD_HOME=$PALM_RAD_HOME
|
||||
export RAD_SEED=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee
|
||||
|
||||
rad auth
|
||||
radicle-node --listen 0.0.0.0:3446 --git-daemon 0.0.0.0:4446 &
|
||||
|
||||
### ALICE ###
|
||||
|
||||
export RAD_HOME=$ALICE_RAD_HOME
|
||||
export RAD_SEED=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
|
||||
|
||||
export GIT_AUTHOR_NAME="Alice Liddell"
|
||||
export GIT_AUTHOR_EMAIL="alice@radicle.xyz"
|
||||
export GIT_COMMITTER_NAME="Alice Liddell"
|
||||
export GIT_COMMITTER_EMAIL="alice@radicle.xyz"
|
||||
|
||||
rad auth
|
||||
|
||||
cd $ALICE_CHECKOUT
|
||||
|
||||
git clone $TEST_REPO_PATH
|
||||
cd $TEST_REPO_NAME
|
||||
|
||||
git checkout main
|
||||
rad init --name "source-browsing" \
|
||||
--description "Git repository for source browsing tests" \
|
||||
--default-branch "main" \
|
||||
--no-confirm
|
||||
|
||||
git checkout feature/branch
|
||||
git push rad
|
||||
|
||||
git checkout orphaned-branch
|
||||
git push rad
|
||||
|
||||
radicle-node --listen 0.0.0.0:3444 --git-daemon 0.0.0.0:4444 \
|
||||
--connect z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8@0.0.0.0:3446 &
|
||||
|
||||
sleep 1
|
||||
|
||||
### PALM CLONE ###
|
||||
|
||||
cd $PALM_CHECKOUT
|
||||
export RAD_HOME=$PALM_RAD_HOME
|
||||
rad clone rad:zKtT7DmF9H34KkvcKj9PHW19WzjT
|
||||
|
||||
### BOB ###
|
||||
|
||||
export GIT_AUTHOR_NAME="Bob Belcher"
|
||||
export GIT_AUTHOR_EMAIL="bob@radicle.xyz"
|
||||
export GIT_COMMITTER_NAME="Bob Belcher"
|
||||
export GIT_COMMITTER_EMAIL="bob@radicle.xyz"
|
||||
export GIT_COMMITTER_DATE="Mon Dec 21 14:00 2022 +0100"
|
||||
|
||||
export RAD_HOME=$BOB_RAD_HOME
|
||||
export RAD_SEED=fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe
|
||||
|
||||
rad auth
|
||||
rad auth
|
||||
radicle-node --listen 0.0.0.0:3445 --git-daemon 0.0.0.0:4445 \
|
||||
--connect z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8@0.0.0.0:3446 &
|
||||
|
||||
sleep 2
|
||||
|
||||
cd $BOB_CHECKOUT
|
||||
rad clone rad:zKtT7DmF9H34KkvcKj9PHW19WzjT
|
||||
|
||||
cd $TEST_REPO_NAME
|
||||
echo "Updated readme" > README.md
|
||||
git add README.md
|
||||
git commit --message "Update readme" --date "$GIT_COMMITTER_DATE"
|
||||
git push rad
|
||||
|
||||
### WAIT FOR SYNC WITH PALM ###
|
||||
|
||||
sleep 2
|
||||
|
||||
cd $BASE_PATH
|
||||
tar -cjf palm.tar.bz2 --exclude "post-receive" --exclude "pre-receive" -C $PALM_RAD_HOME .
|
||||
|
||||
killall radicle-node
|
||||
killall git-daemon
|
||||
@@ -1,128 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
REV=40bdc662b2d48cbfaa87182b21457ef8f861b04a
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
FIXTURE=$REPO_ROOT/tests/fixtures/seeds/palm.tar.bz2
|
||||
WORKSPACE=$REPO_ROOT/tests/tmp/palm
|
||||
PASSPHRASE=asdf
|
||||
CONTAINER_NAME=radicle-http-api-with-fixtures
|
||||
HTTP_API_BINARY=radicle-http-api
|
||||
|
||||
show_usage() {
|
||||
echo
|
||||
echo "Starts a http-api backend with test fixtures."
|
||||
echo
|
||||
echo "USAGE:"
|
||||
echo " run-http-api-with-fixtures [-b|d|h|n]"
|
||||
echo
|
||||
echo "OPTIONS:"
|
||||
echo " -b --binary Use a ${HTTP_API_BINARY} binary that is in PATH to avoid using Docker."
|
||||
echo " -d --detach Daemonize the docker process."
|
||||
echo " -h --help Print this Help."
|
||||
echo " -n --non-interactive Run in non-interactive mode, no user prompts."
|
||||
echo
|
||||
}
|
||||
|
||||
prompt_workspace_removal() {
|
||||
echo "This will irrevocably destroy the following directories:"
|
||||
echo
|
||||
echo $WORKSPACE
|
||||
echo
|
||||
|
||||
read -r -p "Are you sure you want to continue? [yes/no]: " confirm
|
||||
case "$confirm" in
|
||||
[yY][eE][sS])
|
||||
rm -rf $WORKSPACE
|
||||
echo "Done"
|
||||
;;
|
||||
*)
|
||||
echo "Ok, I won't touch your data."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
prepare_workspace() {
|
||||
echo
|
||||
echo "Unpacking fixture $FIXTURE"
|
||||
mkdir -p $WORKSPACE
|
||||
tar -xf $FIXTURE -C $WORKSPACE
|
||||
}
|
||||
|
||||
run_docker() {
|
||||
echo "Starting docker at container $CONTAINER_NAME at $REV"
|
||||
echo " http-api --root $WORKSPACE --passphrase $PASSPHRASE"
|
||||
echo
|
||||
|
||||
exec docker run \
|
||||
--init \
|
||||
--publish 8777:8777 \
|
||||
--rm \
|
||||
--name $CONTAINER_NAME \
|
||||
--volume $WORKSPACE:/app/radicle \
|
||||
"$@" \
|
||||
"gcr.io/radicle-services/http-api:$REV" \
|
||||
--passphrase $PASSPHRASE
|
||||
}
|
||||
|
||||
run_binary() {
|
||||
if ! [ -x "$(command -v $HTTP_API_BINARY)" ]; then
|
||||
echo
|
||||
echo "Couldn't find the $HTTP_API_BINARY binary in your PATH."
|
||||
echo "You can compile it from source:"
|
||||
echo " 👉 https://github.com/radicle-dev/radicle-client-services/tree/${REV}/http-api"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Starting $HTTP_API_BINARY"
|
||||
echo " $HTTP_API_BINARY --listen 0.0.0.0:8777 --root ${WORKSPACE} --passphrase $PASSPHRASE"
|
||||
echo
|
||||
|
||||
$HTTP_API_BINARY --listen 0.0.0.0:8777 --root $WORKSPACE --passphrase $PASSPHRASE
|
||||
}
|
||||
|
||||
BINARY=false
|
||||
NON_INTERACTIVE=false
|
||||
DETACH=false
|
||||
|
||||
while [ $# -ne 0 ]; do
|
||||
case "$1" in
|
||||
--binary | -b)
|
||||
BINARY=true
|
||||
;;
|
||||
--detach | -d)
|
||||
DETACH=true
|
||||
;;
|
||||
--non-interactive | -n)
|
||||
NON_INTERACTIVE=true
|
||||
;;
|
||||
*)
|
||||
show_usage
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
shift
|
||||
done
|
||||
|
||||
if [ "$NON_INTERACTIVE" = true ]; then
|
||||
rm -rf $WORKSPACE
|
||||
prepare_workspace
|
||||
else
|
||||
prompt_workspace_removal
|
||||
prepare_workspace
|
||||
fi
|
||||
|
||||
if [ "$BINARY" = true ]; then
|
||||
run_binary
|
||||
else
|
||||
if [ "$DETACH" = true ]; then
|
||||
run_docker --detach
|
||||
else
|
||||
run_docker
|
||||
fi
|
||||
fi
|
||||
@@ -4,7 +4,7 @@ set -e
|
||||
REV=f1de61ad8897a845f2be49e3e2b2951bc678b678
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
FIXTURE=$REPO_ROOT/tests/fixtures/seeds/palm-heartwood.tar.bz2
|
||||
FIXTURE=$REPO_ROOT/tests/fixtures/seeds/palm.tar.bz2
|
||||
WORKSPACE=$REPO_ROOT/tests/tmp/palm
|
||||
PASSPHRASE=asdf
|
||||
BINARY_PATH="$REPO_ROOT/tests/tmp"
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@
|
||||
import Home from "@app/views/home/Index.svelte";
|
||||
import Session from "@app/views/session/Index.svelte";
|
||||
import Projects from "@app/views/projects/View.svelte";
|
||||
import Seeds from "@app/views/seeds/Routes.svelte";
|
||||
import Seeds from "@app/views/seeds/View.svelte";
|
||||
|
||||
const activeRouteStore = router.activeRouteStore;
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
{#if $activeRouteStore.resource === "home"}
|
||||
<Home />
|
||||
{:else if $activeRouteStore.resource === "seeds"}
|
||||
<Seeds host={$activeRouteStore.params.host} />
|
||||
<Seeds hostAndPort={$activeRouteStore.params.host} />
|
||||
{:else if $activeRouteStore.resource === "session"}
|
||||
<Session activeRoute={$activeRouteStore} />
|
||||
{:else if $activeRouteStore.resource === "projects"}
|
||||
|
||||
@@ -3,17 +3,13 @@
|
||||
|
||||
import { closeFocused } from "@app/components/Floating.svelte";
|
||||
import { sessionStore, disconnect } from "@app/lib/session";
|
||||
import { toClipboard } from "@app/lib/utils";
|
||||
import { toClipboard, formatNodeId } from "@app/lib/utils";
|
||||
|
||||
import Avatar from "@app/components/Comment/Avatar.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Floating from "@app/components/Floating.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
|
||||
function formatId(id: string) {
|
||||
return id.substring(0, 4) + " – " + id.substring(id.length - 4, id.length);
|
||||
}
|
||||
|
||||
let icon: "clipboard-small" | "checkmark-small" = "clipboard-small";
|
||||
|
||||
const restoreIcon = debounce(() => {
|
||||
@@ -43,7 +39,7 @@
|
||||
position: absolute;
|
||||
right: 5rem;
|
||||
top: 5rem;
|
||||
width: 16.5rem;
|
||||
width: 15rem;
|
||||
}
|
||||
.info {
|
||||
align-items: flex-start;
|
||||
@@ -118,7 +114,6 @@
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-tiny);
|
||||
user-select: none;
|
||||
width: 11rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
.disconnect {
|
||||
@@ -171,7 +166,7 @@
|
||||
title={$sessionStore.publicKey} />
|
||||
</div>
|
||||
<div class="user-id txt-small">
|
||||
{formatId($sessionStore.publicKey)}
|
||||
{formatNodeId($sessionStore.publicKey)}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -214,7 +209,7 @@
|
||||
}
|
||||
}}>
|
||||
<div class="id">
|
||||
{$sessionStore.publicKey}
|
||||
{formatNodeId($sessionStore.publicKey)}
|
||||
</div>
|
||||
<div class="id-clipboard">
|
||||
<Icon name={icon} />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import * as modal from "@app/lib/modal";
|
||||
import Link from "@app/components/Link.svelte";
|
||||
import Modal from "@app/components/Modal.svelte";
|
||||
import { formatRadicleId, getSeedEmoji } from "@app/lib/utils";
|
||||
import { formatRepositoryId, getSeedEmoji } from "@app/lib/utils";
|
||||
|
||||
export let query: string;
|
||||
export let results: ProjectResult[];
|
||||
@@ -48,7 +48,7 @@
|
||||
{getSeedEmoji(project.seed.host)} {project.info.name}
|
||||
</span>
|
||||
<span class="id">
|
||||
{formatRadicleId(project.info.id)}
|
||||
{formatRepositoryId(project.info.id)}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Author } from "@app/lib/cobs";
|
||||
|
||||
import {
|
||||
formatNodeId,
|
||||
formatRadicleId,
|
||||
formatTimestamp,
|
||||
} from "@app/lib/utils";
|
||||
import { formatNodeId, formatTimestamp } from "@app/lib/utils";
|
||||
|
||||
export let author: Author;
|
||||
export let timestamp: number;
|
||||
@@ -37,13 +33,9 @@
|
||||
</style>
|
||||
|
||||
<span class="authorship txt-tiny">
|
||||
<span class="id highlight layout-desktop">
|
||||
{window.HEARTWOOD ? formatNodeId(author.id) : formatRadicleId(author.id)}
|
||||
</span>
|
||||
<span class="id highlight layout-desktop">{formatNodeId(author.id)}</span>
|
||||
<span class="id highlight layout-mobile">
|
||||
{window.HEARTWOOD
|
||||
? formatNodeId(author.id).replace("did:key:", "")
|
||||
: formatRadicleId(author.id)}
|
||||
{formatNodeId(author.id).replace("did:key:", "")}
|
||||
</span>
|
||||
<span class="caption"> {caption} </span>
|
||||
<span class="date">
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { createIcon } from "@app/lib/blockies";
|
||||
import { isPeerId, isRadicleId } from "@app/lib/utils";
|
||||
import { isNodeId } from "@app/lib/utils";
|
||||
|
||||
export let title: string;
|
||||
export let source: string;
|
||||
export let inline = false;
|
||||
export let grayscale = false;
|
||||
|
||||
function handleMissingFile() {
|
||||
console.warn("Not able to locate", source);
|
||||
@@ -23,10 +22,9 @@
|
||||
return avatar.toDataURL();
|
||||
}
|
||||
|
||||
if (isRadicleId(source) || isPeerId(source)) {
|
||||
if (isNodeId(source)) {
|
||||
source = createContainer(source);
|
||||
}
|
||||
grayscale = isPeerId(title) || isRadicleId(title);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -41,9 +39,6 @@
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.grayscale {
|
||||
filter: grayscale();
|
||||
}
|
||||
.inline {
|
||||
display: inline-block !important;
|
||||
width: 1rem;
|
||||
@@ -58,5 +53,4 @@
|
||||
class="avatar"
|
||||
alt="avatar"
|
||||
on:error={handleMissingFile}
|
||||
class:inline
|
||||
class:grayscale />
|
||||
class:inline />
|
||||
|
||||
+18
-37
@@ -1,56 +1,37 @@
|
||||
{
|
||||
"reactions": ["👍", "👎", "😄", "🎉", "🙁", "🚀", "👀"],
|
||||
"seeds": {
|
||||
"defaultHttpdPort": 443,
|
||||
"defaultHttpdScheme": "https",
|
||||
"defaultNodePort": 8776,
|
||||
"pinned": [
|
||||
{
|
||||
"host": "willow.radicle.garden",
|
||||
"emoji": "🪵"
|
||||
},
|
||||
{ "host": "pine.radicle.garden", "emoji": "🌲" },
|
||||
{ "host": "maple.radicle.garden", "emoji": "🍁" }
|
||||
"host": "seed.radicle.xyz",
|
||||
"emoji": "🌱"
|
||||
}
|
||||
]
|
||||
},
|
||||
"projects": {
|
||||
"pinned": [
|
||||
{
|
||||
"name": "solmate",
|
||||
"id": "rad:git:hnrkbgczcfh9ycjtdfynmqyg478bqrso1hnty",
|
||||
"seed": "willow.radicle.garden"
|
||||
"name": "radicle-interface",
|
||||
"id": "rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5",
|
||||
"seed": "seed.radicle.xyz"
|
||||
},
|
||||
{
|
||||
"name": "openzeppelin-contracts",
|
||||
"id": "rad:git:hnrkbx9xzjg8tf9hj9uh5qdxwuyjddsxyaw6o",
|
||||
"seed": "willow.radicle.garden"
|
||||
"name": "heartwood",
|
||||
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
|
||||
"seed": "seed.radicle.xyz"
|
||||
},
|
||||
{
|
||||
"name": "haskell-language-server",
|
||||
"id": "rad:git:hnrkf1jaje1e93rua64zgphg7zb14qo9zexgy",
|
||||
"seed": "pine.radicle.garden"
|
||||
"name": "rips",
|
||||
"id": "rad:z3trNYnLWS11cJWC6BbxDs5niGo82",
|
||||
"seed": "seed.radicle.xyz"
|
||||
},
|
||||
{
|
||||
"name": "rx",
|
||||
"id": "rad:git:hnrk8aoks6w3tdbcwt9dqnor9aqy8tjha3k5o",
|
||||
"seed": "maple.radicle.garden"
|
||||
},
|
||||
{
|
||||
"name": "astrolabe",
|
||||
"id": "rad:git:hnrkrwewct7pb1biwmf59qa3rzod8mauundzy",
|
||||
"seed": "willow.radicle.garden"
|
||||
},
|
||||
{
|
||||
"name": "svelte-dappkit",
|
||||
"id": "rad:git:hnrkgcm1e588ewy7iiq4abrrgtf5ioq577eno",
|
||||
"seed": "maple.radicle.garden"
|
||||
},
|
||||
{
|
||||
"name": "tmyxer",
|
||||
"id": "rad:git:hnrkp5jgfxwxrffjsp148fyzdc457pa9ja38y",
|
||||
"seed": "pine.radicle.garden"
|
||||
},
|
||||
{
|
||||
"name": "hexade",
|
||||
"id": "rad:git:hnrkxj511yr48oo3usrt4jzfoucj88zphpryo",
|
||||
"seed": "pine.radicle.garden"
|
||||
"name": "radicle-git",
|
||||
"id": "rad:zMKWcgFLjUSVBBVmxbcMzzokRxub",
|
||||
"seed": "seed.radicle.xyz"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
-2
@@ -10,8 +10,6 @@ declare global {
|
||||
PLAYWRIGHT: boolean;
|
||||
HASH_ROUTING: boolean;
|
||||
|
||||
HEARTWOOD: boolean;
|
||||
|
||||
// APP_CONFIG is set from within Playwright tests at runtime.
|
||||
// To better understand how it works together, have a look at:
|
||||
// tests/support/fixtures.ts
|
||||
|
||||
+5
-15
@@ -1,26 +1,18 @@
|
||||
import { defaultSeedPort } from "@app/lib/seed";
|
||||
import { isLocal } from "@app/lib/utils";
|
||||
|
||||
export interface Host {
|
||||
host: string;
|
||||
port: number | null;
|
||||
port: number;
|
||||
scheme: string;
|
||||
}
|
||||
|
||||
export class Request {
|
||||
path: string;
|
||||
base: string;
|
||||
protocol: string;
|
||||
port: number;
|
||||
|
||||
constructor(path: string, api: Host) {
|
||||
this.port = api.port || defaultSeedPort;
|
||||
if (window.HEARTWOOD) {
|
||||
this.base = api.host + "/api";
|
||||
} else {
|
||||
this.base = api.host;
|
||||
}
|
||||
this.port = api.port;
|
||||
this.base = `${api.scheme}://${api.host}/api`;
|
||||
this.path = path.startsWith("/") ? path.slice(1) : path;
|
||||
this.protocol = isLocal(api.host) ? "http://" : "https://";
|
||||
}
|
||||
|
||||
async get(
|
||||
@@ -97,9 +89,7 @@ export class Request {
|
||||
|
||||
// Creates a URL with an eventual query string and port.
|
||||
private createUrl(search?: string): string {
|
||||
const baseUrl = this.path
|
||||
? `${this.protocol}${this.base}/v1/${this.path}`
|
||||
: `${this.protocol}${this.base}`;
|
||||
const baseUrl = this.path ? `${this.base}/v1/${this.path}` : this.base;
|
||||
|
||||
const url = new URL(search ? `${baseUrl}?${search}` : baseUrl);
|
||||
url.port = String(this.port);
|
||||
|
||||
@@ -5,7 +5,6 @@ export interface Comment<R = null> {
|
||||
body: string;
|
||||
reactions: Record<string, number>;
|
||||
timestamp: number;
|
||||
replies: R; // TODO: Remove for Heartwood migration
|
||||
replyTo: R;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import configJson from "@app/config.json";
|
||||
export interface Config {
|
||||
reactions: string[];
|
||||
seeds: {
|
||||
defaultHttpdPort: number;
|
||||
defaultNodePort: number;
|
||||
defaultHttpdScheme: string;
|
||||
pinned: { host: string; emoji: string }[];
|
||||
};
|
||||
projects: {
|
||||
@@ -19,6 +22,9 @@ function getConfig(): Config {
|
||||
return {
|
||||
reactions: [],
|
||||
seeds: {
|
||||
defaultHttpdPort: 8080,
|
||||
defaultHttpdScheme: "http",
|
||||
defaultNodePort: 8776,
|
||||
pinned: [],
|
||||
},
|
||||
projects: { pinned: [] },
|
||||
|
||||
+4
-19
@@ -14,7 +14,6 @@ export interface IIssue {
|
||||
author: Author;
|
||||
title: string;
|
||||
state: State;
|
||||
comment: Comment; // TODO: Remove this after we have migrated to Heartwood.
|
||||
discussion: Thread[];
|
||||
tags: string[];
|
||||
assignees: string[];
|
||||
@@ -35,7 +34,6 @@ export interface Comment<R = null> {
|
||||
body: string;
|
||||
reactions: Record<string, number>;
|
||||
timestamp: number;
|
||||
replies: R; // TODO: Remove this after we have migrated to Heartwood.
|
||||
replyTo: R;
|
||||
}
|
||||
|
||||
@@ -59,7 +57,6 @@ export class Issue {
|
||||
author: Author;
|
||||
title: string;
|
||||
state: State;
|
||||
comment: Comment; // TODO: Remove this after we have migrated to Heartwood.
|
||||
discussion: Thread[];
|
||||
tags: string[];
|
||||
assignees: string[];
|
||||
@@ -70,29 +67,17 @@ export class Issue {
|
||||
this.author = issue.author;
|
||||
this.title = issue.title;
|
||||
this.state = issue.state;
|
||||
this.comment = issue.comment; // TODO: Remove this after we have migrated to Heartwood.
|
||||
this.discussion = issue.discussion;
|
||||
this.tags = issue.tags;
|
||||
this.assignees = issue.assignees;
|
||||
if (window.HEARTWOOD) {
|
||||
this.timestamp = issue.discussion[0].timestamp;
|
||||
} else {
|
||||
this.timestamp = issue.timestamp;
|
||||
}
|
||||
this.timestamp = issue.discussion[0].timestamp;
|
||||
}
|
||||
|
||||
// Counts the amount of comments and replies in a discussion
|
||||
countComments(): number {
|
||||
if (window.HEARTWOOD) {
|
||||
return this.discussion.reduce(acc => {
|
||||
return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop.
|
||||
}, 0);
|
||||
} else {
|
||||
return this.discussion.reduce((acc, comment) => {
|
||||
if (comment.replies) return acc + comment.replies.length + 1; // We add all replies and 1 forathe comment in this loop.
|
||||
return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop.
|
||||
}, 0);
|
||||
}
|
||||
return this.discussion.reduce(acc => {
|
||||
return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop.
|
||||
}, 0);
|
||||
}
|
||||
|
||||
static async createIssue(
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
import type { Author, PeerInfo } from "@app/lib/cobs";
|
||||
import type { Comment, Thread } from "@app/lib/issue";
|
||||
import type { Commit } from "@app/lib/commit";
|
||||
import type { Diff, DiffStats } from "@app/lib/diff";
|
||||
import type { Host } from "@app/lib/api";
|
||||
import type { PeerId, Id } from "@app/lib/project";
|
||||
|
||||
import { Request } from "@app/lib/api";
|
||||
|
||||
export interface IPatch {
|
||||
id: string;
|
||||
author: Author;
|
||||
title: string;
|
||||
state: string;
|
||||
target: string;
|
||||
labels: string[];
|
||||
revisions: Revision[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export enum PatchTab {
|
||||
Timeline = "timeline",
|
||||
Diff = "diff",
|
||||
}
|
||||
|
||||
export interface Revision {
|
||||
id: string;
|
||||
peer: PeerId;
|
||||
base: string;
|
||||
oid: string;
|
||||
comment: Comment;
|
||||
discussion: Thread[];
|
||||
reviews: Record<Id, Review>;
|
||||
merges: Merge[];
|
||||
changeset: {
|
||||
diff: Diff;
|
||||
commits: Commit[];
|
||||
stats: DiffStats;
|
||||
} | null;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
author: Author;
|
||||
verdict: Verdict | null;
|
||||
comment: Thread;
|
||||
inline: CodeComment[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type Verdict = "accept" | "reject";
|
||||
|
||||
export interface CodeComment {
|
||||
location: CodeLocation;
|
||||
comment: Comment;
|
||||
}
|
||||
|
||||
export interface CodeLocation {
|
||||
lines: number;
|
||||
commit: string;
|
||||
blob: string;
|
||||
}
|
||||
|
||||
export interface Merge {
|
||||
peer: PeerInfo;
|
||||
commit: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export function groupPatches(patches: Patch[]) {
|
||||
return patches.reduce(
|
||||
(acc: { [state: string]: Patch[] }, patch) => {
|
||||
acc[patch.state].push(patch);
|
||||
return acc;
|
||||
},
|
||||
{ proposed: [] as Patch[], draft: [] as Patch[], archived: [] as Patch[] },
|
||||
);
|
||||
}
|
||||
|
||||
export class Patch implements IPatch {
|
||||
id: string;
|
||||
author: Author;
|
||||
title: string;
|
||||
state: string;
|
||||
target: string;
|
||||
labels: string[];
|
||||
revisions: Revision[];
|
||||
timestamp: number;
|
||||
|
||||
constructor(patch: IPatch) {
|
||||
this.id = patch.id;
|
||||
this.author = patch.author;
|
||||
this.title = patch.title;
|
||||
this.state = patch.state;
|
||||
this.target = patch.target;
|
||||
this.labels = patch.labels;
|
||||
this.revisions = patch.revisions;
|
||||
this.timestamp = patch.timestamp;
|
||||
}
|
||||
|
||||
// Counts the amount of comments and replies in a discussion
|
||||
countComments(rev: number): number {
|
||||
if (window.HEARTWOOD) {
|
||||
return this.revisions[rev].discussion.reduce(acc => {
|
||||
return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop.
|
||||
}, 0);
|
||||
} else {
|
||||
return this.revisions[rev].discussion.reduce((acc, comment) => {
|
||||
if (comment.replies) return acc + comment.replies.length + 1; // We add all replies and 1 for each comment in this loop.
|
||||
return acc + 1; // If there are no replies, we simply add 1 for the comment in this loop.
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
createTimeline(rev: number) {
|
||||
const timeline: TimelineElement[] = [];
|
||||
const comment: TimelineElement = {
|
||||
type: TimelineType.Comment,
|
||||
timestamp: this.revisions[rev].comment.timestamp,
|
||||
inner: this.revisions[rev].comment,
|
||||
};
|
||||
const discussions = this.revisions[rev].discussion.map(
|
||||
(comment): TimelineElement => {
|
||||
return {
|
||||
type: TimelineType.Thread,
|
||||
timestamp: comment.timestamp,
|
||||
inner: comment,
|
||||
};
|
||||
},
|
||||
);
|
||||
const reviews = Object.entries(this.revisions[rev].reviews).map(
|
||||
([, review]): TimelineElement => {
|
||||
return {
|
||||
type: TimelineType.Review,
|
||||
timestamp: review.timestamp,
|
||||
inner: review,
|
||||
};
|
||||
},
|
||||
);
|
||||
const merges = this.revisions[rev].merges.map((merge): TimelineElement => {
|
||||
return {
|
||||
type: TimelineType.Merge,
|
||||
timestamp: merge.timestamp,
|
||||
inner: merge,
|
||||
};
|
||||
});
|
||||
timeline.push(comment, ...discussions, ...merges, ...reviews);
|
||||
return timeline.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
static async getPatches(id: string, host: Host): Promise<Patch[]> {
|
||||
const response = await new Request(`projects/${id}/patches`, host).get();
|
||||
for (const patch of response) {
|
||||
patch.author["id"] = patch.author["urn"];
|
||||
delete patch.author["urn"];
|
||||
}
|
||||
|
||||
return response.map((patch: IPatch) => new Patch(patch));
|
||||
}
|
||||
|
||||
static async getPatch(id: string, patch: string, host: Host): Promise<Patch> {
|
||||
const response = await new Request(
|
||||
`projects/${id}/patches/${patch}`,
|
||||
host,
|
||||
).get();
|
||||
|
||||
response.author["id"] = response.author["urn"];
|
||||
delete response.author["urn"];
|
||||
|
||||
for (const revision of response.revisions) {
|
||||
if (revision.changeset?.diff) {
|
||||
revision.changeset.diff["added"] = revision.changeset.diff["created"];
|
||||
delete revision.changeset.diff["created"];
|
||||
|
||||
revision.comment.author["id"] = revision.comment.author["urn"];
|
||||
delete revision.comment.author["urn"];
|
||||
|
||||
revision.changeset.stats["insertions"] =
|
||||
revision.changeset.stats["additions"];
|
||||
delete revision.changeset.stats["additions"];
|
||||
|
||||
for (const kind of ["added", "deleted", "modified"]) {
|
||||
for (const file of revision.changeset.diff[kind]) {
|
||||
for (const hunk of file.diff.hunks) {
|
||||
for (const line of hunk.lines) {
|
||||
if (line["lineNumOld"]) {
|
||||
line["lineNoOld"] = line["lineNumOld"];
|
||||
delete line["lineNumOld"];
|
||||
}
|
||||
|
||||
if (line["lineNumNew"]) {
|
||||
line["lineNoNew"] = line["lineNumNew"];
|
||||
delete line["lineNumNew"];
|
||||
}
|
||||
|
||||
if (line["lineNum"]) {
|
||||
line["lineNo"] = line["lineNum"];
|
||||
delete line["lineNum"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Patch(response);
|
||||
}
|
||||
}
|
||||
|
||||
export const formatVerdict = (verdict: string | null): string => {
|
||||
switch (verdict) {
|
||||
case "accept":
|
||||
return "approved this revision";
|
||||
|
||||
case "reject":
|
||||
return "rejected this revision";
|
||||
|
||||
default:
|
||||
return "reviewed and left a comment";
|
||||
}
|
||||
};
|
||||
|
||||
export enum TimelineType {
|
||||
Comment,
|
||||
Thread,
|
||||
Review,
|
||||
Merge,
|
||||
}
|
||||
|
||||
export type TimelineElement =
|
||||
| {
|
||||
type: TimelineType.Thread;
|
||||
inner: Thread;
|
||||
timestamp: number;
|
||||
}
|
||||
| {
|
||||
type: TimelineType.Comment;
|
||||
inner: Comment;
|
||||
timestamp: number;
|
||||
}
|
||||
| {
|
||||
type: TimelineType.Merge;
|
||||
inner: Merge;
|
||||
timestamp: number;
|
||||
}
|
||||
| {
|
||||
type: TimelineType.Review;
|
||||
inner: Review;
|
||||
timestamp: number;
|
||||
};
|
||||
+18
-152
@@ -2,28 +2,15 @@ import type { Commit, CommitHeader, CommitsHistory } from "@app/lib/commit";
|
||||
import type { Host } from "@app/lib/api";
|
||||
import type { ProjectResult } from "@app/lib/search";
|
||||
|
||||
import * as utils from "@app/lib/utils";
|
||||
import { Request } from "@app/lib/api";
|
||||
import { Seed, defaultSeedPort } from "@app/lib/seed";
|
||||
import { isFulfilled, isOid, isRadicleId } from "@app/lib/utils";
|
||||
import { Seed } from "@app/lib/seed";
|
||||
import { isFulfilled, isOid, isRepositoryId } from "@app/lib/utils";
|
||||
|
||||
export type Id = string;
|
||||
export type PeerId = string;
|
||||
export type Branches = { [key: string]: string };
|
||||
export type MaybeBlob = Blob | undefined;
|
||||
export type MaybeTree = Tree | undefined;
|
||||
|
||||
// TODO: Remove this after we have migrated to Heartwood.
|
||||
export type Delegate =
|
||||
| {
|
||||
type: "indirect";
|
||||
id: Id;
|
||||
ids: PeerId[];
|
||||
}
|
||||
| {
|
||||
type: "direct";
|
||||
id: PeerId;
|
||||
};
|
||||
|
||||
// Enumerates the space below the Header component in the projects View component
|
||||
export enum ProjectContent {
|
||||
Tree,
|
||||
@@ -31,8 +18,6 @@ export enum ProjectContent {
|
||||
Commit,
|
||||
Issues,
|
||||
Issue,
|
||||
Patches,
|
||||
Patch,
|
||||
}
|
||||
|
||||
export interface ProjectInfo {
|
||||
@@ -42,7 +27,6 @@ export interface ProjectInfo {
|
||||
description: string;
|
||||
defaultBranch: string;
|
||||
delegates: string[];
|
||||
remotes: PeerId[]; // TODO: Remove this after we have migrated to Heartwood.
|
||||
patches: {
|
||||
proposed: number;
|
||||
draft: number;
|
||||
@@ -56,45 +40,30 @@ export interface ProjectInfo {
|
||||
|
||||
export interface Tree {
|
||||
path: string;
|
||||
info: EntryInfo; // TODO: Remove this after we have migrated to Heartwood.
|
||||
entries: Array<Entry>;
|
||||
stats: Stats;
|
||||
name: string;
|
||||
kind: Kind;
|
||||
lastCommit: CommitHeader;
|
||||
}
|
||||
|
||||
// TODO: Remove "TREE" and "BLOB" after we have migrated to Heartwood.
|
||||
type Kind = "tree" | "blob" | "TREE" | "BLOB";
|
||||
type Kind = "tree" | "blob";
|
||||
|
||||
export interface Stats {
|
||||
commits: number;
|
||||
contributors: number;
|
||||
}
|
||||
|
||||
// TODO: Remove this after we have migrated to Heartwood.
|
||||
export interface EntryInfo {
|
||||
name: string;
|
||||
objectType: Kind;
|
||||
lastCommit: CommitHeader;
|
||||
}
|
||||
|
||||
export interface Entry {
|
||||
path: string;
|
||||
info: EntryInfo; // TODO: Remove this after we have migrated to Heartwood.
|
||||
name: string;
|
||||
kind: Kind;
|
||||
lastCommit: CommitHeader;
|
||||
}
|
||||
|
||||
export interface Blob {
|
||||
binary?: boolean;
|
||||
html?: boolean;
|
||||
binary: boolean;
|
||||
content: string;
|
||||
path: string;
|
||||
info: EntryInfo; // TODO: Remove this after we have migrated to Heartwood.
|
||||
name: string;
|
||||
kind: Kind;
|
||||
lastCommit: CommitHeader;
|
||||
}
|
||||
|
||||
@@ -107,7 +76,7 @@ export interface Person {
|
||||
}
|
||||
|
||||
export interface Peer {
|
||||
id: PeerId;
|
||||
id: string;
|
||||
person?: Person;
|
||||
delegate: boolean;
|
||||
}
|
||||
@@ -161,7 +130,6 @@ export class Project implements ProjectInfo {
|
||||
description: string;
|
||||
defaultBranch: string;
|
||||
delegates: string[];
|
||||
remotes: PeerId[]; // TODO: Remove this after we have migrated to Heartwood.
|
||||
seed: Seed;
|
||||
peers: Peer[];
|
||||
branches: Branches;
|
||||
@@ -188,7 +156,6 @@ export class Project implements ProjectInfo {
|
||||
this.description = info.description;
|
||||
this.defaultBranch = info.defaultBranch;
|
||||
this.delegates = info.delegates;
|
||||
this.remotes = info.remotes; // TODO: Remove this after we have migrated to Heartwood.
|
||||
this.seed = seed;
|
||||
this.peers = peers;
|
||||
this.branches = branches;
|
||||
@@ -211,14 +178,7 @@ export class Project implements ProjectInfo {
|
||||
}
|
||||
|
||||
static async getInfo(nameOrId: string, host: Host): Promise<ProjectInfo> {
|
||||
if (window.HEARTWOOD) {
|
||||
return await new Request(`projects/${nameOrId}`, host).get();
|
||||
} else {
|
||||
const result = await new Request(`projects/${nameOrId}`, host).get();
|
||||
result["id"] = result["urn"];
|
||||
delete result["urn"];
|
||||
return result;
|
||||
}
|
||||
return await new Request(`projects/${nameOrId}`, host).get();
|
||||
}
|
||||
|
||||
static async getProjects(
|
||||
@@ -232,17 +192,7 @@ export class Project implements ProjectInfo {
|
||||
"per-page": opts?.perPage,
|
||||
page: opts?.page,
|
||||
};
|
||||
if (window.HEARTWOOD) {
|
||||
return await new Request("projects", host).get(params);
|
||||
} else {
|
||||
const results = await new Request("projects", host).get(params);
|
||||
results.forEach((result: { [x: string]: any }) => {
|
||||
result["id"] = result["urn"];
|
||||
delete result["urn"];
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
return await new Request("projects", host).get(params);
|
||||
}
|
||||
|
||||
static async getDelegateProjects(
|
||||
@@ -295,20 +245,6 @@ export class Project implements ProjectInfo {
|
||||
const result = await new Request(`projects/${id}/commits`, host).get(
|
||||
params,
|
||||
);
|
||||
if (!window.HEARTWOOD) {
|
||||
for (const commit of result.headers) {
|
||||
commit.commit = commit.header;
|
||||
delete commit.header;
|
||||
|
||||
commit.commit.committer.time = commit.commit.committerTime;
|
||||
delete commit.commit.committerTime;
|
||||
|
||||
commit.commit.id = commit.commit.sha1;
|
||||
delete commit.commit.sha1;
|
||||
}
|
||||
result.commits = result.headers;
|
||||
delete result.headers;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -325,45 +261,6 @@ export class Project implements ProjectInfo {
|
||||
this.seed.addr,
|
||||
).get();
|
||||
|
||||
if (!window.HEARTWOOD) {
|
||||
result.commit = result.header;
|
||||
delete result.header;
|
||||
|
||||
result.commit.committer.time = result.commit.committerTime;
|
||||
delete result.commit.committerTime;
|
||||
|
||||
result.commit.id = result.commit.sha1;
|
||||
delete result.commit.sha1;
|
||||
|
||||
result.stats["insertions"] = result.stats["additions"];
|
||||
delete result.stats["additions"];
|
||||
result.diff["added"] = result.diff["created"];
|
||||
delete result.diff["created"];
|
||||
|
||||
for (const kind of ["added", "deleted", "modified"]) {
|
||||
for (const file of result.diff[kind]) {
|
||||
for (const hunk of file.diff.hunks) {
|
||||
for (const line of hunk.lines) {
|
||||
if (line["lineNumOld"]) {
|
||||
line["lineNoOld"] = line["lineNumOld"];
|
||||
delete line["lineNumOld"];
|
||||
}
|
||||
|
||||
if (line["lineNumNew"]) {
|
||||
line["lineNoNew"] = line["lineNumNew"];
|
||||
delete line["lineNumNew"];
|
||||
}
|
||||
|
||||
if (line["lineNum"]) {
|
||||
line["lineNo"] = line["lineNum"];
|
||||
delete line["lineNum"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -373,17 +270,6 @@ export class Project implements ProjectInfo {
|
||||
`projects/${this.id}/tree/${commit}/${path}`,
|
||||
this.seed.addr,
|
||||
).get();
|
||||
if (!window.HEARTWOOD) {
|
||||
if (result.info.lastCommit) {
|
||||
result.info.lastCommit.id = result.info.lastCommit.sha1;
|
||||
delete result.info.lastCommit.sha1;
|
||||
}
|
||||
for (const entry of result.entries) {
|
||||
if (entry.info.lastCommit) {
|
||||
entry.info.lastCommit.id = entry.info.lastCommit.sha1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -392,10 +278,6 @@ export class Project implements ProjectInfo {
|
||||
`projects/${this.id}/blob/${commit}/${path}`,
|
||||
this.seed.addr,
|
||||
).get();
|
||||
if (!window.HEARTWOOD) {
|
||||
result.info.lastCommit.id = result.info.lastCommit.sha1;
|
||||
delete result.info.lastCommit.sha1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -404,10 +286,6 @@ export class Project implements ProjectInfo {
|
||||
`projects/${this.id}/readme/${commit}`,
|
||||
this.seed.addr,
|
||||
).get();
|
||||
if (!window.HEARTWOOD && result.info.lastCommit) {
|
||||
result.info.lastCommit.id = result.info.lastCommit.sha1;
|
||||
delete result.info.lastCommit.sha1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -416,31 +294,20 @@ export class Project implements ProjectInfo {
|
||||
seedHost: string,
|
||||
peer?: string,
|
||||
): Promise<Project> {
|
||||
const [host, port] = seedHost.includes(":")
|
||||
? seedHost.split(":")
|
||||
: [seedHost, defaultSeedPort];
|
||||
let seed: Seed | undefined = undefined;
|
||||
|
||||
const seed = await Seed.lookup(host, Number(port)).catch(() => {
|
||||
throw new Error("Couldn't load project");
|
||||
});
|
||||
|
||||
if (!seed?.valid) {
|
||||
throw new Error("Couldn't load project: invalid seed");
|
||||
try {
|
||||
seed = await Seed.lookup(utils.extractHost(seedHost));
|
||||
} catch (error) {
|
||||
throw new Error(`Couldn't load project: ${error}`);
|
||||
}
|
||||
|
||||
const info = await Project.getInfo(id, seed.addr);
|
||||
id = isRadicleId(id) ? id : info.id;
|
||||
id = isRepositoryId(id) ? id : info.id;
|
||||
|
||||
let peers: Peer[] = [];
|
||||
|
||||
if (window.HEARTWOOD) {
|
||||
peers = await Project.getRemotes(id, seed.addr);
|
||||
} else {
|
||||
// Older versions of http-api don't include the ID.
|
||||
if (!info.id) info.id = id;
|
||||
|
||||
peers = info.delegates ? await Project.getRemotes(id, seed.addr) : [];
|
||||
}
|
||||
peers = await Project.getRemotes(id, seed.addr);
|
||||
|
||||
let remote: Remote = {
|
||||
heads: info.head ? { [info.defaultBranch]: info.head } : {},
|
||||
@@ -458,15 +325,14 @@ export class Project implements ProjectInfo {
|
||||
}
|
||||
|
||||
static async getMulti(
|
||||
projs: { nameOrId: Id; seed: string }[],
|
||||
projs: { nameOrId: string; seed: Host }[],
|
||||
): Promise<ProjectResult[]> {
|
||||
const promises = [];
|
||||
|
||||
for (const proj of projs) {
|
||||
const seed = { host: proj.seed, port: null };
|
||||
promises.push(
|
||||
Project.getInfo(proj.nameOrId, seed).then(info => {
|
||||
return { info, seed };
|
||||
Project.getInfo(proj.nameOrId, proj.seed).then(info => {
|
||||
return { info, seed: proj.seed };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -252,10 +252,6 @@ export function routeToPath(route: Route) {
|
||||
return `${hostPrefix}/${route.params.id}${peer}/commits${suffix}`;
|
||||
} else if (route.params.view.resource === "history") {
|
||||
return `${hostPrefix}/${route.params.id}${peer}/history${suffix}`;
|
||||
} else if (route.params.view.resource === "patches") {
|
||||
return `${hostPrefix}/${route.params.id}${peer}/patches${suffix}`;
|
||||
} else if (route.params.view.resource === "patch") {
|
||||
return `${hostPrefix}/${route.params.id}${peer}/patches/${route.params.view.params.patch}`;
|
||||
} else if (
|
||||
route.params.view.resource === "issues" &&
|
||||
route.params.view.params?.view.resource === "new"
|
||||
@@ -325,29 +321,6 @@ function resolveProjectRoute(
|
||||
search: undefined,
|
||||
route: segments.join("/"),
|
||||
};
|
||||
} else if (content === "patches") {
|
||||
const patch = segments.shift();
|
||||
if (patch) {
|
||||
return {
|
||||
view: { resource: "patch", params: { patch } },
|
||||
id,
|
||||
seed,
|
||||
peer,
|
||||
path: undefined,
|
||||
search: undefined,
|
||||
revision: undefined,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
view: { resource: "patches" },
|
||||
id,
|
||||
seed,
|
||||
peer,
|
||||
search: sanitizeQueryString(url.search),
|
||||
path: undefined,
|
||||
revision: undefined,
|
||||
};
|
||||
}
|
||||
} else if (content === "issues") {
|
||||
const issueOrAction = segments.shift();
|
||||
if (issueOrAction === "new") {
|
||||
|
||||
@@ -20,9 +20,7 @@ export interface ProjectsParams {
|
||||
params?: {
|
||||
view: { resource: "new" };
|
||||
};
|
||||
}
|
||||
| { resource: "patch"; params: { patch: string } }
|
||||
| { resource: "patches" };
|
||||
};
|
||||
seed: string;
|
||||
hash?: string;
|
||||
line?: string;
|
||||
|
||||
+6
-2
@@ -21,11 +21,15 @@ export async function searchProjectsAndProfiles(
|
||||
try {
|
||||
const projectOnSeeds = config.seeds.pinned.map(seed => ({
|
||||
nameOrId: query,
|
||||
seed: seed.host,
|
||||
seed: {
|
||||
host: seed.host,
|
||||
port: config.seeds.defaultHttpdPort,
|
||||
scheme: config.seeds.defaultHttpdScheme,
|
||||
},
|
||||
}));
|
||||
|
||||
// The query is a radicle project ID.
|
||||
if (utils.isRadicleId(query)) {
|
||||
if (utils.isRepositoryId(query)) {
|
||||
const projects = await Project.getMulti(projectOnSeeds);
|
||||
|
||||
if (projects.length === 0) {
|
||||
|
||||
+17
-108
@@ -4,103 +4,32 @@ import * as proj from "@app/lib/project";
|
||||
import { Request } from "@app/lib/api";
|
||||
import { assert } from "@app/lib/error";
|
||||
import { getSeedEmoji } from "@app/lib/utils";
|
||||
import { isDomain } from "@app/lib/utils";
|
||||
import { config } from "@app/lib/config";
|
||||
|
||||
export interface Stats {
|
||||
projects: { count: number };
|
||||
users: { count: number };
|
||||
}
|
||||
|
||||
export class InvalidSeed {
|
||||
valid = false as const;
|
||||
|
||||
host?: string;
|
||||
id?: string;
|
||||
|
||||
constructor(host?: string, id?: string) {
|
||||
this.host = host;
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultSeedPort = window.HEARTWOOD ? 8080 : 8777;
|
||||
export const defaultNodePort = 8776;
|
||||
// TODO: Remove this after we have migrated to Heartwood.
|
||||
export const defaultGitPort = 443;
|
||||
|
||||
export class Seed {
|
||||
valid = true as const;
|
||||
|
||||
addr: { host: string; port: number | null };
|
||||
git: { host: string; port: number | null }; // TODO: Remove this after we have migrated to Heartwood.
|
||||
node: { host: string; id: string; port: number };
|
||||
addr: Host;
|
||||
node: Host & { id: string };
|
||||
|
||||
version?: string;
|
||||
emoji: string;
|
||||
|
||||
constructor(seed: {
|
||||
host: string;
|
||||
id: string;
|
||||
git?: string | null; // TODO: Remove this after we have migrated to Heartwood.
|
||||
addr?: string | null;
|
||||
version?: string | null;
|
||||
}) {
|
||||
assert(isDomain(seed.host), `invalid seed host: ${seed.host}`);
|
||||
if (window.HEARTWOOD) {
|
||||
assert(/^[a-zA-Z0-9]+$/.test(seed.id), `invalid seed id ${seed.id}`);
|
||||
} else {
|
||||
assert(/^[a-z0-9]+$/.test(seed.id), `invalid seed id ${seed.id}`);
|
||||
}
|
||||
constructor(seed: { host: Host; id: string; version?: string }) {
|
||||
assert(/^[a-zA-Z0-9]+$/.test(seed.id), `invalid seed id ${seed.id}`);
|
||||
|
||||
let _seed = null;
|
||||
let _git = null; // TODO: Remove this after we have migrated to Heartwood.
|
||||
let _seedPort: number | null = defaultSeedPort;
|
||||
let _gitPort: number | null = defaultGitPort; // TODO: Remove this after we have migrated to Heartwood.
|
||||
this.emoji = getSeedEmoji(seed.host.host);
|
||||
|
||||
if (seed.addr) {
|
||||
try {
|
||||
const url = new URL(seed.addr);
|
||||
_seed = url.hostname;
|
||||
|
||||
if (url.port) {
|
||||
_seedPort = Number(url.port);
|
||||
} else if (url.protocol === "http:" && url.port === "") {
|
||||
_seedPort = 80;
|
||||
}
|
||||
if (url.protocol === "https:" && url.port === "") {
|
||||
_seedPort = 443;
|
||||
} else {
|
||||
_seedPort = null;
|
||||
}
|
||||
} catch {
|
||||
_seed = seed.addr;
|
||||
}
|
||||
assert(isDomain(_seed), `invalid seed host ${_seed}`);
|
||||
}
|
||||
|
||||
if (window.HEARTWOOD) {
|
||||
if (seed.git) {
|
||||
try {
|
||||
const url = new URL(seed.git);
|
||||
_git = url.hostname;
|
||||
_gitPort = url.port ? Number(url.port) : null;
|
||||
} catch {
|
||||
_git = seed.git;
|
||||
}
|
||||
assert(isDomain(_git), `invalid seed git host ${_git}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.emoji = getSeedEmoji(seed.host);
|
||||
|
||||
// The `_seed` being more specific takes
|
||||
// precedence over the `host`, if available.
|
||||
_seed = _seed ?? seed.host;
|
||||
_git = _git ?? seed.host; // TODO: Remove this after we have migrated to Heartwood.
|
||||
|
||||
this.addr = { host: _seed, port: _seedPort };
|
||||
this.git = { host: _git, port: _gitPort }; // TODO: Remove this after we have migrated to Heartwood.
|
||||
this.node = { host: seed.host, id: seed.id, port: defaultNodePort };
|
||||
this.addr = seed.host;
|
||||
this.node = {
|
||||
host: seed.host.host,
|
||||
id: seed.id,
|
||||
port: config.seeds.defaultNodePort,
|
||||
scheme: seed.host.scheme,
|
||||
};
|
||||
|
||||
if (seed.version) {
|
||||
this.version = seed.version;
|
||||
@@ -128,14 +57,7 @@ export class Seed {
|
||||
? await proj.Project.getDelegateProjects(id, this.addr, { perPage })
|
||||
: await proj.Project.getProjects(this.addr, { perPage });
|
||||
|
||||
if (window.HEARTWOOD) {
|
||||
return result;
|
||||
} else {
|
||||
return result.map((project: proj.ProjectInfo) => ({
|
||||
...project,
|
||||
id: project.id,
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async getStats(): Promise<{
|
||||
@@ -146,36 +68,23 @@ export class Seed {
|
||||
}
|
||||
|
||||
static async getNode(host: Host): Promise<{ id: string }> {
|
||||
if (window.HEARTWOOD) {
|
||||
return new Request("/node", host).get();
|
||||
} else {
|
||||
return new Request("/peer", host).get();
|
||||
}
|
||||
return new Request("/node", host).get();
|
||||
}
|
||||
|
||||
static async getInfo(host: Host): Promise<{ version: string }> {
|
||||
return new Request("/", host).get();
|
||||
}
|
||||
|
||||
static async lookup(
|
||||
hostname: string,
|
||||
port: number = defaultSeedPort,
|
||||
): Promise<Seed> {
|
||||
const host = { host: hostname, port };
|
||||
static async lookup(host: Host): Promise<Seed> {
|
||||
const [info, node] = await Promise.all([
|
||||
Seed.getInfo(host),
|
||||
Seed.getNode(host),
|
||||
]);
|
||||
|
||||
return new Seed({
|
||||
host: hostname,
|
||||
id: node.id,
|
||||
version: info.version,
|
||||
addr: `https://${host.host}:${host.port}`,
|
||||
host,
|
||||
});
|
||||
}
|
||||
|
||||
static async lookupMulti(hostnames: string[]): Promise<Seed[]> {
|
||||
return await Promise.all(hostnames.map(h => Seed.lookup(h)));
|
||||
}
|
||||
}
|
||||
|
||||
+79
-55
@@ -1,8 +1,9 @@
|
||||
import type { Host } from "@app/lib/api";
|
||||
|
||||
import md5 from "md5";
|
||||
import bs58 from "bs58";
|
||||
import twemojiModule from "twemoji";
|
||||
|
||||
import { assert } from "@app/lib/error";
|
||||
import { base } from "@app/lib/router";
|
||||
import { config } from "@app/lib/config";
|
||||
|
||||
@@ -43,23 +44,7 @@ export function formatLocationHash(hash: string | null): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function formatSeedId(id: string): string {
|
||||
return id.substring(0, 6) + "…" + id.substring(id.length - 6, id.length);
|
||||
}
|
||||
|
||||
export function formatRadicleId(id: string): string {
|
||||
assert(isRadicleId(id));
|
||||
|
||||
if (window.HEARTWOOD) {
|
||||
return id.substring(0, 10) + "…" + id.substring(id.length - 6, id.length);
|
||||
} else {
|
||||
return id.substring(0, 14) + "…" + id.substring(id.length - 6, id.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Parses a NID into an object of prefix and pubkey,
|
||||
// since prefix can be undefined
|
||||
export function parseNid(
|
||||
export function parseNodeId(
|
||||
nid: string,
|
||||
): { prefix: string; pubkey: string } | undefined {
|
||||
const match = /^(did:key:)?(z[a-zA-Z0-9]+)$/.exec(nid);
|
||||
@@ -77,19 +62,52 @@ export function parseNid(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Format a Node Identifier (NID), also represents users or peers
|
||||
export function formatNodeId(nid: string): string {
|
||||
const parsedNid = parseNid(nid);
|
||||
if (parsedNid) {
|
||||
const { prefix, pubkey } = parsedNid;
|
||||
const formattedPubKey =
|
||||
pubkey.substring(0, 6) +
|
||||
"…" +
|
||||
pubkey.substring(pubkey.length - 6, pubkey.length);
|
||||
return `${prefix}${formattedPubKey}`;
|
||||
export function parseRepositoryId(
|
||||
rid: string,
|
||||
): { prefix: string; pubkey: string } | undefined {
|
||||
const match = /^(rad:)?(z[a-zA-Z0-9]+)$/.exec(rid);
|
||||
if (match) {
|
||||
const hex = bs58.decode(match[2].substring(1));
|
||||
if (hex.byteLength !== 20) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { prefix: match[1] || "rad:", pubkey: match[2] };
|
||||
}
|
||||
|
||||
return nid;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isNodeId(input: string): boolean {
|
||||
return Boolean(parseNodeId(input));
|
||||
}
|
||||
|
||||
export function isRepositoryId(input: string): boolean {
|
||||
return Boolean(parseRepositoryId(input));
|
||||
}
|
||||
|
||||
export function formatNodeId(id: string): string {
|
||||
const parsedId = parseNodeId(id);
|
||||
|
||||
if (parsedId) {
|
||||
return truncateId(parsedId.prefix, parsedId.pubkey);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
export function formatRepositoryId(id: string): string {
|
||||
const parsedId = parseRepositoryId(id);
|
||||
|
||||
if (parsedId) {
|
||||
return truncateId(parsedId.prefix, parsedId.pubkey);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
function truncateId(prefix: string, pubkey: string): string {
|
||||
return `${prefix}${pubkey.substring(0, 6) + "…" + pubkey.slice(-6)}`;
|
||||
}
|
||||
|
||||
export function formatCommit(oid: string): string {
|
||||
@@ -191,20 +209,6 @@ export const formatTimestamp = (
|
||||
return new Date(timestamp).toUTCString();
|
||||
};
|
||||
|
||||
// Check whether the input is a Radicle ID.
|
||||
export function isRadicleId(input: string): boolean {
|
||||
if (window.HEARTWOOD) {
|
||||
return /^(did:key:)?[a-zA-Z0-9]+$/.test(input);
|
||||
} else {
|
||||
return /^rad:[a-z]+:[a-zA-Z0-9]+$/.test(input);
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the input is a Radicle Peer ID.
|
||||
export function isPeerId(input: string): boolean {
|
||||
return /^h[a-zA-Z0-9]+$/.test(input);
|
||||
}
|
||||
|
||||
// Check whether the input is a SHA1 commit.
|
||||
export function isOid(input: string): boolean {
|
||||
return /^[a-fA-F0-9]{40}$/.test(input);
|
||||
@@ -221,15 +225,6 @@ export function isFulfilled<T>(
|
||||
return input.status === "fulfilled";
|
||||
}
|
||||
|
||||
// Parse a Radicle Id.
|
||||
export function parseRadicleId(id: string): string {
|
||||
if (window.HEARTWOOD) {
|
||||
return id.replace(/^rad:/, "");
|
||||
} else {
|
||||
return id.replace(/^rad:[a-z]+:/, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Get amount of days passed between two dates without including the end date
|
||||
export function getDaysPassed(from: Date, to: Date): number {
|
||||
return Math.floor((to.getTime() - from.getTime()) / (24 * 60 * 60 * 1000));
|
||||
@@ -257,8 +252,8 @@ export function isMarkdownPath(path: string): boolean {
|
||||
return /\.(md|mkd|markdown)$/i.test(path);
|
||||
}
|
||||
|
||||
// Check whether the given input string is a domain, eg. `alt-clients.radicle.xyz.
|
||||
// Also accepts in dev env 0.0.0.0 as domain
|
||||
// Check whether the given input string is a domain, eg. seed.radicle.xyz.
|
||||
// Also accepts in dev env 0.0.0.0 as domain.
|
||||
export function isDomain(input: string): boolean {
|
||||
return (
|
||||
(/^[a-z][a-z0-9.-]+$/.test(input) && /\.[a-z]+$/.test(input)) ||
|
||||
@@ -268,7 +263,11 @@ export function isDomain(input: string): boolean {
|
||||
|
||||
// Check whether the given address is a local host address.
|
||||
export function isLocal(addr: string): boolean {
|
||||
return addr === "127.0.0.1" || addr === "0.0.0.0";
|
||||
return (
|
||||
addr.startsWith("127.0.0.1") ||
|
||||
addr.startsWith("0.0.0.0") ||
|
||||
addr.startsWith("radicle.local")
|
||||
);
|
||||
}
|
||||
|
||||
// Get the gravatar URL of an email.
|
||||
@@ -299,3 +298,28 @@ export function twemoji(
|
||||
className: `txt-emoji`,
|
||||
});
|
||||
}
|
||||
|
||||
export function extractHost(origin: string): Host {
|
||||
if (
|
||||
origin === "radicle.local" ||
|
||||
origin === "radicle.local:8080" ||
|
||||
origin === "0.0.0.0" ||
|
||||
origin === "0.0.0.0:8080" ||
|
||||
origin === "127.0.0.1" ||
|
||||
origin === "127.0.0.1:8080"
|
||||
) {
|
||||
return { host: "0.0.0.0", port: 8080, scheme: "http" };
|
||||
} else if (origin.includes(":")) {
|
||||
return {
|
||||
host: origin.split(":")[0],
|
||||
port: Number(origin.split(":")[1]),
|
||||
scheme: config.seeds.defaultHttpdScheme,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
host: origin,
|
||||
port: config.seeds.defaultHttpdPort,
|
||||
scheme: config.seeds.defaultHttpdScheme,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
? Project.getMulti(
|
||||
config.projects.pinned.map(project => ({
|
||||
nameOrId: project.id,
|
||||
seed: project.seed,
|
||||
seed: {
|
||||
host: project.seed,
|
||||
port: config.seeds.defaultHttpdPort,
|
||||
scheme: config.seeds.defaultHttpdScheme,
|
||||
},
|
||||
})),
|
||||
)
|
||||
: Promise.resolve([]);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
export let line: string | undefined = undefined;
|
||||
|
||||
const fileExtension = blob.path.split(".").pop() ?? "";
|
||||
const lastCommit = window.HEARTWOOD ? blob.lastCommit : blob.info.lastCommit;
|
||||
const lastCommit = blob.lastCommit;
|
||||
|
||||
const parentDir = blob.path
|
||||
.match(/^.*\/|/)
|
||||
@@ -215,7 +215,7 @@
|
||||
<span class="file-name">
|
||||
<span class="txt-faded">{parentDir}</span>
|
||||
​
|
||||
<span>{window.HEARTWOOD ? blob.name : blob.info.name}</span>
|
||||
<span>{blob.name}</span>
|
||||
</span>
|
||||
<div class="right">
|
||||
{#if isMarkdown}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<script lang="ts">
|
||||
import * as utils from "@app/lib/utils";
|
||||
import Clipboard from "@app/components/Clipboard.svelte";
|
||||
import Floating from "@app/components/Floating.svelte";
|
||||
import { closeFocused } from "@app/components/Floating.svelte";
|
||||
import { parseRepositoryId } from "@app/lib/utils";
|
||||
|
||||
export let seedHost: string;
|
||||
export let id: string;
|
||||
export let name: string;
|
||||
|
||||
$: radCloneUrl = `rad clone rad://${seedHost}/${utils.parseRadicleId(id)}`;
|
||||
$: gitCloneUrl = `https://${seedHost}/${utils.parseRadicleId(id)}.git`;
|
||||
$: radCloneUrl = `rad clone ${id}`;
|
||||
$: gitCloneUrl = `git clone https://${seedHost}/${
|
||||
parseRepositoryId(id)?.pubkey ?? id
|
||||
}.git ${name}`;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
</header>
|
||||
<Changeset
|
||||
stats={window.HEARTWOOD ? commit.diff.stats : commit.stats}
|
||||
stats={commit.diff.stats}
|
||||
diff={commit.diff}
|
||||
on:browse={onBrowse} />
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import CommitAuthorship from "./CommitAuthorship.svelte";
|
||||
import CommitVerifiedBadge from "./CommitVerifiedBadge.svelte";
|
||||
|
||||
export let commit: CommitMetadata;
|
||||
|
||||
@@ -99,11 +98,6 @@
|
||||
<CommitAuthorship {commit} noDelegate />
|
||||
</div>
|
||||
<div class="column-right">
|
||||
{#if !window.HEARTWOOD && commit.context.committer}
|
||||
<div class="layout-desktop">
|
||||
<CommitVerifiedBadge {commit} />
|
||||
</div>
|
||||
{/if}
|
||||
<span class="hash txt-highlight">{formatCommit(commit.commit.id)}</span>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
import * as router from "@app/lib/router";
|
||||
import BranchSelector from "@app/views/projects/BranchSelector.svelte";
|
||||
import CloneButton from "@app/views/projects/CloneButton.svelte";
|
||||
import HeaderToggleLabel from "@app/views/projects/HeaderToggleLabel.svelte";
|
||||
import PeerSelector from "@app/views/projects/PeerSelector.svelte";
|
||||
import { closeFocused } from "@app/components/Floating.svelte";
|
||||
import HeaderToggleLabel from "@app/views/projects/HeaderToggleLabel.svelte";
|
||||
import { config } from "@app/lib/config";
|
||||
|
||||
export let activeRoute: ProjectRoute;
|
||||
export let project: Project;
|
||||
@@ -21,7 +22,7 @@
|
||||
|
||||
// Switches between project views.
|
||||
const toggleContent = (
|
||||
input: "patches" | "issues" | "history",
|
||||
input: "issues" | "history",
|
||||
keepSourceInPath: boolean,
|
||||
) => {
|
||||
router.updateProjectRoute({
|
||||
@@ -51,7 +52,7 @@
|
||||
};
|
||||
|
||||
function goToSeed() {
|
||||
if (seed.addr.port) {
|
||||
if (seed.addr.port !== config.seeds.defaultHttpdPort) {
|
||||
router.push({
|
||||
resource: "seeds",
|
||||
params: { host: `${seed.addr.host}:${seed.addr.port}` },
|
||||
@@ -98,10 +99,8 @@
|
||||
{revision}
|
||||
on:branchChanged={event => updateRevision(event.detail)} />
|
||||
|
||||
{#if window.HEARTWOOD && seed.addr.host}
|
||||
<CloneButton seedHost={seed.addr.host} {id} />
|
||||
{:else if seed.git.host}
|
||||
<CloneButton seedHost={seed.git.host} {id} />
|
||||
{#if seed.addr.host}
|
||||
<CloneButton seedHost={seed.addr.host} {id} name={project.name} />
|
||||
{/if}
|
||||
|
||||
<span>
|
||||
@@ -131,14 +130,6 @@
|
||||
<span class="txt-bold">{project.issues.open ?? 0}</span>
|
||||
issue(s)
|
||||
</HeaderToggleLabel>
|
||||
<HeaderToggleLabel
|
||||
ariaLabel="Patch count"
|
||||
clickable
|
||||
active={activeRoute.params.view.resource === "patches"}
|
||||
on:click={() => toggleContent("patches", false)}>
|
||||
<span class="txt-bold">{project.patches.proposed ?? 0}</span>
|
||||
patch(es)
|
||||
</HeaderToggleLabel>
|
||||
<HeaderToggleLabel ariaLabel="Contributor count">
|
||||
<span class="txt-bold">{tree.stats.contributors}</span>
|
||||
contributor(s)
|
||||
|
||||
@@ -93,9 +93,6 @@
|
||||
color: var(--color-negative);
|
||||
background-color: var(--color-negative-2);
|
||||
}
|
||||
.replies {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
.assignee {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -142,21 +139,8 @@
|
||||
</header>
|
||||
<main>
|
||||
<div class="comments">
|
||||
{#if !window.HEARTWOOD}
|
||||
<Comment comment={issue.comment} {getImage} />
|
||||
{/if}
|
||||
|
||||
{#each issue.discussion as comment}
|
||||
<Comment {comment} {getImage} />
|
||||
{#if !window.HEARTWOOD}
|
||||
{#if comment.replies}
|
||||
<div class="replies">
|
||||
{#each comment.replies as reply}
|
||||
<Comment comment={reply} {getImage} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="metadata layout-desktop">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import TextInput from "@app/components/TextInput.svelte";
|
||||
import Textarea from "@app/components/Textarea.svelte";
|
||||
import { Issue } from "@app/lib/issue";
|
||||
import { canonicalize, formatNodeId, parseNid } from "@app/lib/utils";
|
||||
import { canonicalize, formatNodeId, parseNodeId } from "@app/lib/utils";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let session: Session;
|
||||
@@ -29,13 +29,13 @@
|
||||
let preview: boolean = false;
|
||||
|
||||
function handleAddAssignee() {
|
||||
const nid = parseNid(assignee);
|
||||
if (nid) {
|
||||
if (assignees.includes(nid.pubkey)) {
|
||||
const nodeId = parseNodeId(assignee);
|
||||
if (nodeId) {
|
||||
if (assignees.includes(nodeId.pubkey)) {
|
||||
assigneeCaption = "This user is already assigned";
|
||||
return;
|
||||
}
|
||||
assignees.push(nid.pubkey);
|
||||
assignees.push(nodeId.pubkey);
|
||||
assignees = assignees;
|
||||
assignee = "";
|
||||
} else {
|
||||
@@ -185,7 +185,6 @@
|
||||
author: { id: session.publicKey },
|
||||
body: issueText,
|
||||
reactions: {},
|
||||
replies: null,
|
||||
replyTo: null,
|
||||
timestamp: Date.now(),
|
||||
}}
|
||||
@@ -245,7 +244,7 @@
|
||||
on:input={() => (assigneeCaption = undefined)}
|
||||
placeholder="Assign this issue"
|
||||
validationMessage={assigneeCaption}
|
||||
valid={Boolean(parseNid(assignee))} />
|
||||
valid={Boolean(parseNodeId(assignee))} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="section txt-small">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import type { Tab } from "@app/components/TabBar.svelte";
|
||||
|
||||
import * as router from "@app/lib/router";
|
||||
import * as utils from "@app/lib/utils";
|
||||
import { capitalize } from "@app/lib/utils";
|
||||
import { groupIssues } from "@app/lib/issue";
|
||||
import { sessionStore } from "@app/lib/session";
|
||||
@@ -79,7 +80,7 @@
|
||||
active={state} />
|
||||
</div>
|
||||
<HeaderToggleLabel
|
||||
disabled={!$sessionStore}
|
||||
disabled={!$sessionStore || !utils.isLocal(project.seed.host)}
|
||||
on:click={() => {
|
||||
router.updateProjectRoute({
|
||||
view: {
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Project } from "@app/lib/project";
|
||||
|
||||
import { capitalize } from "@app/lib/utils";
|
||||
import { Patch, PatchTab } from "@app/lib/patch";
|
||||
import { formatObjectId } from "@app/lib/cobs";
|
||||
import Authorship from "@app/components/Authorship.svelte";
|
||||
|
||||
import Changeset from "./SourceBrowser/Changeset.svelte";
|
||||
import PatchSideBar from "./Patch/PatchSideBar.svelte";
|
||||
import PatchTabBar from "./Patch/PatchTabBar.svelte";
|
||||
import PatchTimeline from "./Patch/PatchTimeline.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
import * as router from "@app/lib/router";
|
||||
|
||||
export let patch: Patch;
|
||||
export let project: Project;
|
||||
|
||||
const onSwitch = ({ detail }: { detail: PatchTab }) => {
|
||||
activeTab = detail;
|
||||
};
|
||||
|
||||
const onRevisionChanged = ({ detail }: { detail: string }) => {
|
||||
revisionNumber = parseInt(detail);
|
||||
};
|
||||
|
||||
const onBrowse = (event: { detail: string }, revision: string) => {
|
||||
router.updateProjectRoute({
|
||||
view: { resource: "tree" },
|
||||
revision,
|
||||
path: event.detail,
|
||||
});
|
||||
};
|
||||
|
||||
let activeTab = PatchTab.Timeline;
|
||||
let revisionNumber = patch.revisions.length - 1;
|
||||
|
||||
$: revision = patch.revisions[revisionNumber];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
header {
|
||||
padding: 1rem;
|
||||
background: var(--color-foreground-1);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.patch {
|
||||
padding: 0 2rem 0 8rem;
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.summary-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.summary-title {
|
||||
display: flex;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
.id {
|
||||
font-size: var(--font-size-tiny);
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
.summary-state {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 1.25rem;
|
||||
}
|
||||
.proposed {
|
||||
color: var(--color-positive-6);
|
||||
background-color: var(--color-positive-1);
|
||||
}
|
||||
.draft {
|
||||
color: var(--color-positive-6);
|
||||
background-color: var(--color-positive-1);
|
||||
}
|
||||
.archived {
|
||||
color: var(--color-negative-6);
|
||||
background-color: var(--color-negative-1);
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.patch {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="patch">
|
||||
<header>
|
||||
<div class="summary">
|
||||
<div class="summary-left">
|
||||
<span class="summary-title txt-medium">
|
||||
{patch.title}
|
||||
</span>
|
||||
<span class="txt-monospace id layout-desktop">{patch.id}</span>
|
||||
<span class="txt-monospace id layout-mobile">
|
||||
{formatObjectId(patch.id)}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="summary-state"
|
||||
class:proposed={patch.state === "proposed"}
|
||||
class:draft={patch.state === "draft"}
|
||||
class:archived={patch.state === "archived"}>
|
||||
{capitalize(patch.state)}
|
||||
</div>
|
||||
</div>
|
||||
<Authorship
|
||||
author={patch.author}
|
||||
timestamp={patch.timestamp}
|
||||
caption="opened" />
|
||||
</header>
|
||||
|
||||
<PatchTabBar
|
||||
{activeTab}
|
||||
{revisionNumber}
|
||||
revisions={patch.revisions}
|
||||
on:switchTab={onSwitch}
|
||||
on:revisionChanged={onRevisionChanged} />
|
||||
|
||||
<main>
|
||||
{#if activeTab === PatchTab.Timeline}
|
||||
<div class="flex">
|
||||
<PatchTimeline {patch} {revisionNumber} {project} />
|
||||
<PatchSideBar {patch} />
|
||||
</div>
|
||||
{:else if activeTab === PatchTab.Diff && revision.changeset}
|
||||
<Changeset
|
||||
stats={window.HEARTWOOD
|
||||
? revision.changeset.diff.stats
|
||||
: revision.changeset.stats}
|
||||
diff={revision.changeset.diff}
|
||||
on:browse={e => onBrowse(e, revision.oid)} />
|
||||
{:else if activeTab === PatchTab.Diff}
|
||||
<Placeholder emoji="🍳">
|
||||
<span slot="title">No changeset found</span>
|
||||
<span slot="body">
|
||||
We couldn't find a changeset related to this patch or revision
|
||||
</span>
|
||||
</Placeholder>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
@@ -1,54 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Patch } from "@app/lib/patch";
|
||||
|
||||
export let patch: Patch;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.metadata {
|
||||
flex-basis: 18rem;
|
||||
margin-left: 1rem;
|
||||
border-radius: var(--border-radius-medium);
|
||||
font-size: var(--font-size-small);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
.metadata-section {
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px dashed var(--color-foreground-4);
|
||||
}
|
||||
.metadata-section-header {
|
||||
font-size: var(--font-size-small);
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
.metadata-section-body {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.metadata-section-empty {
|
||||
color: var(--color-foreground-6);
|
||||
}
|
||||
.label {
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-tertiary);
|
||||
background-color: var(--color-tertiary-2);
|
||||
padding: 0.25rem 0.75rem;
|
||||
margin-right: 0.5rem;
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="metadata layout-desktop">
|
||||
<div class="metadata-section">
|
||||
<div class="metadata-section-header">Labels</div>
|
||||
<div class="metadata-section-body">
|
||||
{#if patch.labels?.length}
|
||||
{#each patch.labels as label}
|
||||
<span class="label">{label}</span>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="metadata-section-empty">No labels.</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,102 +0,0 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import type { Tab } from "@app/components/TabBar.svelte";
|
||||
|
||||
import Dropdown from "@app/components/Dropdown.svelte";
|
||||
import Floating, { closeFocused } from "@app/components/Floating.svelte";
|
||||
import TabBar from "@app/components/TabBar.svelte";
|
||||
|
||||
import type { Revision } from "@app/lib/patch";
|
||||
import { PatchTab } from "@app/lib/patch";
|
||||
import { formatCommit, formatTimestamp } from "@app/lib/utils";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let revisions: Revision[];
|
||||
export let revisionNumber: number;
|
||||
export let activeTab: PatchTab;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
switchTab: PatchTab;
|
||||
revisionChanged: string;
|
||||
}>();
|
||||
|
||||
const formatRevisionName = (revision: Revision, index: number) => {
|
||||
return `R${index} ${formatCommit(revision.oid)} ${formatTimestamp(
|
||||
revision.timestamp,
|
||||
)}`;
|
||||
};
|
||||
|
||||
const revisionList = Object.values(revisions).map((b, i) => ({
|
||||
key: formatRevisionName(b, i),
|
||||
value: i.toString(),
|
||||
title: `Browse revision ${formatRevisionName(b, i)}`,
|
||||
badge: null,
|
||||
}));
|
||||
|
||||
const onRevisionChange = ({ detail }: { detail: string }) => {
|
||||
closeFocused();
|
||||
dispatch("revisionChanged", detail);
|
||||
};
|
||||
|
||||
let options: Tab<PatchTab>[];
|
||||
$: options = [
|
||||
{
|
||||
title: "Patch",
|
||||
value: PatchTab.Timeline,
|
||||
},
|
||||
{
|
||||
title: "Changeset",
|
||||
value: PatchTab.Diff,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bar {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.revision-toggle {
|
||||
border-radius: var(--border-radius-small);
|
||||
border: none;
|
||||
color: var(--color-foreground-6);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-tiny);
|
||||
height: var(--button-tiny-height);
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
.revision-toggle:hover {
|
||||
background-color: var(--color-foreground-1);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
.revision-toggle:disabled {
|
||||
color: var(--color-foreground-5);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="bar txt-small">
|
||||
<TabBar
|
||||
{options}
|
||||
on:select={e => {
|
||||
dispatch("switchTab", e.detail);
|
||||
}}
|
||||
active={activeTab} />
|
||||
|
||||
<Floating disabled={revisions.length <= 1}>
|
||||
<button
|
||||
slot="toggle"
|
||||
class="txt-small revision-toggle"
|
||||
disabled={revisions.length <= 1}>
|
||||
{formatRevisionName(revisions[revisionNumber], revisionNumber)}
|
||||
</button>
|
||||
|
||||
<svelte:fragment slot="modal">
|
||||
<Dropdown
|
||||
items={revisionList}
|
||||
selected={revisionNumber.toString()}
|
||||
on:select={onRevisionChange} />
|
||||
</svelte:fragment>
|
||||
</Floating>
|
||||
</div>
|
||||
@@ -1,114 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Patch } from "@app/lib/patch";
|
||||
|
||||
import { formatObjectId } from "@app/lib/cobs";
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
import Authorship from "@app/components/Authorship.svelte";
|
||||
|
||||
export let patch: Patch;
|
||||
|
||||
const commentCount = patch.countComments(patch.revisions.length - 1);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.patch-teaser {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-foreground-1);
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
.patch-teaser:hover {
|
||||
background-color: var(--color-foreground-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
.patch-id {
|
||||
color: var(--color-foreground-5);
|
||||
font-size: var(--font-size-tiny);
|
||||
font-family: var(--font-family-monospace);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.column-left {
|
||||
flex: min-content;
|
||||
}
|
||||
.column-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-right: 1rem;
|
||||
flex-basis: 5rem;
|
||||
}
|
||||
.comment-count {
|
||||
color: var(--color-foreground-4);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
.comment-count .emoji {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.state {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.state-icon {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.open {
|
||||
background-color: var(--color-positive);
|
||||
}
|
||||
.closed {
|
||||
background-color: var(--color-negative);
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.column-left {
|
||||
overflow: hidden;
|
||||
}
|
||||
.summary {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="patch-teaser">
|
||||
<div class="state">
|
||||
<div
|
||||
class="state-icon"
|
||||
class:closed={patch.state === "archived"}
|
||||
class:open={patch.state === "proposed"} />
|
||||
</div>
|
||||
<div class="column-left">
|
||||
<div class="summary">
|
||||
<!-- TODO: Truncation not working on overflow -->
|
||||
{patch.title}
|
||||
<span class="patch-id">{formatObjectId(patch.id)}</span>
|
||||
</div>
|
||||
<Authorship
|
||||
caption="opened"
|
||||
author={patch.author}
|
||||
timestamp={patch.timestamp} />
|
||||
</div>
|
||||
{#if commentCount > 0}
|
||||
<div class="column-right">
|
||||
<div class="comment-count">
|
||||
<span class="txt-tiny emoji" use:twemoji>💬</span>
|
||||
<span>{commentCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,73 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Blob, Project } from "@app/lib/project";
|
||||
|
||||
import Authorship from "@app/components/Authorship.svelte";
|
||||
import Comment from "@app/components/Comment.svelte";
|
||||
import Review from "@app/views/projects/Patch/Review.svelte";
|
||||
import { Patch, TimelineType } from "@app/lib/patch";
|
||||
import { canonicalize } from "@app/lib/utils";
|
||||
import { formatSeedId } from "@app/lib/utils";
|
||||
|
||||
export let patch: Patch;
|
||||
export let revisionNumber: number;
|
||||
export let project: Project;
|
||||
|
||||
$: timeline = patch.createTimeline(revisionNumber);
|
||||
|
||||
// Get an image blob based on a relative path.
|
||||
const getImage = async (imagePath: string): Promise<Blob> => {
|
||||
const finalPath = canonicalize(imagePath, "/"); // We only use the root path in issues.
|
||||
const commit = project.branches[project.defaultBranch]; // We suppose that all issues are only looked at on HEAD of the default branch.
|
||||
return project.getBlob(commit, finalPath);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
.replies {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
.element {
|
||||
margin: 0 0 1rem 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="timeline">
|
||||
{#each timeline as element}
|
||||
{#if element.type === TimelineType.Merge && element.inner.peer.person}
|
||||
<div class="element">
|
||||
<Authorship
|
||||
author={{ id: element.inner.peer.person.id }}
|
||||
caption={`merged to ${formatSeedId(element.inner.peer.id)}`}
|
||||
timestamp={element.timestamp} />
|
||||
</div>
|
||||
{:else if element.type === TimelineType.Review && element.inner.author.id}
|
||||
<div class="margin-left">
|
||||
<Review review={element.inner} {getImage} />
|
||||
</div>
|
||||
{:else if element.type === TimelineType.Comment}
|
||||
<div class="margin-left">
|
||||
<!-- Since the element variable only experiences changes on the inner property,
|
||||
this component has to be forced to be rerendered when element.inner changes -->
|
||||
{#key element.inner}
|
||||
<Comment comment={element.inner} {getImage} />
|
||||
{/key}
|
||||
</div>
|
||||
{:else if element.type === TimelineType.Thread}
|
||||
<div class="margin-left">
|
||||
<Comment comment={element.inner} {getImage} />
|
||||
{#if !window.HEARTWOOD}
|
||||
<div class="replies">
|
||||
{#each element.inner.replies as comment}
|
||||
<Comment caption="replied" {comment} {getImage} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,31 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Review } from "@app/lib/patch";
|
||||
import type { Blob } from "@app/lib/project";
|
||||
|
||||
import Authorship from "@app/components/Authorship.svelte";
|
||||
import Comment from "@app/components/Comment.svelte";
|
||||
import { formatVerdict } from "@app/lib/patch";
|
||||
|
||||
export let review: Review;
|
||||
export let getImage: (path: string) => Promise<Blob>;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div {
|
||||
margin: 0 0 1rem 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if review.comment.body}
|
||||
<Comment
|
||||
{getImage}
|
||||
comment={review.comment}
|
||||
caption={formatVerdict(review.verdict)} />
|
||||
{:else}
|
||||
<div>
|
||||
<Authorship
|
||||
author={review.author}
|
||||
timestamp={review.timestamp}
|
||||
caption={formatVerdict(review.verdict)} />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,94 +0,0 @@
|
||||
<script lang="ts" context="module">
|
||||
export type State = "proposed" | "draft" | "archived";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Patch } from "@app/lib/patch";
|
||||
import type { Tab } from "@app/components/TabBar.svelte";
|
||||
import type { Project } from "@app/lib/project";
|
||||
|
||||
import PatchTeaser from "./Patch/PatchTeaser.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
import TabBar from "@app/components/TabBar.svelte";
|
||||
|
||||
import { capitalize } from "@app/lib/utils";
|
||||
import { groupPatches } from "@app/lib/patch";
|
||||
import * as router from "@app/lib/router";
|
||||
|
||||
export let state: State;
|
||||
export let patches: Patch[];
|
||||
export let project: Project;
|
||||
|
||||
let options: Tab<State>[];
|
||||
const sortedPatches = groupPatches(patches);
|
||||
|
||||
$: filteredPatches = sortedPatches[state];
|
||||
$: options = [
|
||||
{
|
||||
value: "proposed",
|
||||
count: project.patches.proposed,
|
||||
},
|
||||
{
|
||||
value: "draft",
|
||||
count: project.patches.draft,
|
||||
},
|
||||
{
|
||||
value: "archived",
|
||||
count: project.patches.archived,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.patches {
|
||||
padding: 0 2rem 0 8rem;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
.patches-list {
|
||||
border-radius: var(--border-radius-small);
|
||||
overflow: hidden;
|
||||
}
|
||||
.teaser:not(:last-child) {
|
||||
border-bottom: 1px dashed var(--color-background);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.patches {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="patches">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<TabBar
|
||||
{options}
|
||||
on:select={e =>
|
||||
router.updateProjectRoute({
|
||||
search: `state=${e.detail}`,
|
||||
})}
|
||||
active={state} />
|
||||
</div>
|
||||
|
||||
{#if filteredPatches.length}
|
||||
<div class="patches-list">
|
||||
{#each filteredPatches as patch}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="teaser"
|
||||
on:click={() => {
|
||||
router.updateProjectRoute({
|
||||
view: { resource: "patch", params: { patch: patch.id } },
|
||||
});
|
||||
}}>
|
||||
<PatchTeaser {patch} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Placeholder emoji="🍂">
|
||||
<div slot="title">{capitalize(state)} patches</div>
|
||||
<div slot="body">No patches matched the current filter</div>
|
||||
</Placeholder>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -21,24 +21,17 @@
|
||||
}[] = [];
|
||||
|
||||
function createTitle(p: Peer): string {
|
||||
const name = p.person?.name ? p.person.name : p.id;
|
||||
const nodeId = formatNodeId(p.id);
|
||||
return p.delegate
|
||||
? `${name} is a delegate of this project`
|
||||
: `${name} is a peer tracked by this seed`;
|
||||
? `${nodeId} is a delegate of this project`
|
||||
: `${nodeId} is a peer tracked by this node`;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
meta = peers.find(p => p.id === peer);
|
||||
items = peers.map(p => {
|
||||
if (!p.person?.name) {
|
||||
console.debug("Not able to resolve peer identity for: ", p.id);
|
||||
}
|
||||
const key = p.person?.name
|
||||
? `<span class="txt-bold">${p.person.name}</span> ${p.id}`
|
||||
: p.id;
|
||||
|
||||
return {
|
||||
key,
|
||||
key: formatNodeId(p.id),
|
||||
value: p.id,
|
||||
title: createTitle(p),
|
||||
badge: p.delegate ? "delegate" : null,
|
||||
@@ -91,7 +84,7 @@
|
||||
<Icon name="fork" />
|
||||
{#if meta}
|
||||
<span class="peer-id">
|
||||
{meta.person?.name ?? formatNodeId(meta.id)}
|
||||
{formatNodeId(meta.id)}
|
||||
</span>
|
||||
{#if meta.delegate}
|
||||
<Badge variant="primary">delegate</Badge>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { PeerId, Project } from "@app/lib/project";
|
||||
import type { Project } from "@app/lib/project";
|
||||
|
||||
import Clipboard from "@app/components/Clipboard.svelte";
|
||||
import DOMPurify from "dompurify";
|
||||
import ProjectLink from "@app/components/ProjectLink.svelte";
|
||||
import { formatSeedId } from "@app/lib/utils";
|
||||
import { formatNodeId } from "@app/lib/utils";
|
||||
|
||||
export let project: Project;
|
||||
export let peer: PeerId | undefined = undefined;
|
||||
export let nodeId: string | undefined = undefined;
|
||||
|
||||
const linkifyDescription = (text: string) => {
|
||||
return text.replaceAll(/(https?:\/\/[^\s]+)/g, `<a href="$1">$1</a>`);
|
||||
@@ -27,7 +27,7 @@
|
||||
margin: 0 0.5rem;
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
.title .peer-id {
|
||||
.title .node-id {
|
||||
color: var(--color-foreground-5);
|
||||
font-weight: var(--font-weight-normal);
|
||||
display: flex;
|
||||
@@ -87,11 +87,11 @@
|
||||
</span>
|
||||
</ProjectLink>
|
||||
</span>
|
||||
{#if peer}
|
||||
<span class="peer-id">
|
||||
{#if nodeId}
|
||||
<span class="node-id">
|
||||
<span class="divider">/</span>
|
||||
<span title={peer}>{formatSeedId(peer)}</span>
|
||||
<Clipboard text={peer} />
|
||||
<span title={nodeId}>{formatNodeId(nodeId)}</span>
|
||||
<Clipboard text={nodeId} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
</script>
|
||||
|
||||
{#each tree.entries as entry (entry.path)}
|
||||
{#if window.HEARTWOOD ? entry.kind === "tree" : entry.info.objectType === "TREE"}
|
||||
{#if entry.kind === "tree"}
|
||||
<Folder
|
||||
{fetchTree}
|
||||
{loadingPath}
|
||||
name={window.HEARTWOOD ? entry.name : entry.info.name}
|
||||
name={entry.name}
|
||||
prefix={`${entry.path}/`}
|
||||
currentPath={path}
|
||||
on:select={onSelect} />
|
||||
@@ -30,7 +30,7 @@
|
||||
<File
|
||||
active={entry.path === path}
|
||||
loading={entry.path === loadingPath}
|
||||
name={window.HEARTWOOD ? entry.name : entry.info.name}
|
||||
name={entry.name}
|
||||
on:click={() => onSelect({ detail: entry.path })} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -74,10 +74,10 @@
|
||||
{:then tree}
|
||||
{#if tree}
|
||||
{#each tree.entries as entry (entry.path)}
|
||||
{#if window.HEARTWOOD ? entry.kind === "tree" : entry.info.objectType === "TREE"}
|
||||
{#if entry.kind === "tree"}
|
||||
<svelte:self
|
||||
{fetchTree}
|
||||
name={window.HEARTWOOD ? entry.name : entry.info.name}
|
||||
name={entry.name}
|
||||
on:select={onSelectFile}
|
||||
prefix={`${entry.path}/`}
|
||||
{loadingPath}
|
||||
@@ -86,7 +86,7 @@
|
||||
<File
|
||||
active={entry.path === currentPath}
|
||||
loading={entry.path === loadingPath}
|
||||
name={window.HEARTWOOD ? entry.name : entry.info.name}
|
||||
name={entry.name}
|
||||
on:click={() => {
|
||||
onSelectFile({ detail: entry.path });
|
||||
}} />
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { ProjectRoute } from "@app/lib/router/definitions";
|
||||
import type { State as IssueState } from "./Issues.svelte";
|
||||
import type { State as PatchState } from "./Patches.svelte";
|
||||
|
||||
import * as issue from "@app/lib/issue";
|
||||
import * as patch from "@app/lib/patch";
|
||||
import * as proj from "@app/lib/project";
|
||||
import * as router from "@app/lib/router";
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
@@ -20,8 +18,6 @@
|
||||
import Issues from "./Issues.svelte";
|
||||
import Message from "@app/components/Message.svelte";
|
||||
import NewIssue from "./Issue/New.svelte";
|
||||
import Patch from "./Patch.svelte";
|
||||
import Patches from "./Patches.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
import ProjectMeta from "./ProjectMeta.svelte";
|
||||
|
||||
@@ -33,7 +29,6 @@
|
||||
|
||||
$: searchParams = new URLSearchParams(activeRoute.params.search || "");
|
||||
$: issueFilter = (searchParams.get("state") as IssueState) || "open";
|
||||
$: patchFilter = (searchParams.get("state") as PatchState) || "proposed";
|
||||
|
||||
const getProject = async (id: string, seed: string, peer?: string) => {
|
||||
const project = await proj.Project.get(id, seed, peer);
|
||||
@@ -120,7 +115,7 @@
|
||||
</main>
|
||||
{:then project}
|
||||
<main>
|
||||
<ProjectMeta {project} {peer} />
|
||||
<ProjectMeta {project} nodeId={peer} />
|
||||
{#await project.getRoot(revision)}
|
||||
<Loading center />
|
||||
{:then { tree, commit }}
|
||||
@@ -182,26 +177,6 @@
|
||||
<Message error>{e.message}</Message>
|
||||
</div>
|
||||
{/await}
|
||||
{:else if activeRoute.params.view.resource === "patches"}
|
||||
{#await patch.Patch.getPatches(project.id, project.seed.addr)}
|
||||
<Loading center />
|
||||
{:then patches}
|
||||
<Patches {project} state={patchFilter} {patches} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<Message error>{e.message}</Message>
|
||||
</div>
|
||||
{/await}
|
||||
{:else if activeRoute.params.view.resource === "patch"}
|
||||
{#await patch.Patch.getPatch(project.id, activeRoute.params.view.params.patch, project.seed.addr)}
|
||||
<Loading center />
|
||||
{:then patch}
|
||||
<Patch {project} {patch} />
|
||||
{:catch e}
|
||||
<div class="message">
|
||||
<Message error>{e.message}</Message>
|
||||
</div>
|
||||
{/await}
|
||||
{:else}
|
||||
{unreachable(activeRoute.params.view)}
|
||||
{/if}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="ts">
|
||||
import View from "@app/views/seeds/View.svelte";
|
||||
|
||||
export let host: string;
|
||||
</script>
|
||||
|
||||
{#if host === "radicle.local"}
|
||||
<View hostAndPort={"0.0.0.0"} />
|
||||
{:else}
|
||||
<View hostAndPort={host} />
|
||||
{/if}
|
||||
+20
-16
@@ -1,25 +1,27 @@
|
||||
<script lang="ts">
|
||||
import type { Host } from "@app/lib/api";
|
||||
import type { ProjectInfo } from "@app/lib/project";
|
||||
import type { Stats } from "@app/lib/seed";
|
||||
|
||||
import { Project } from "@app/lib/project";
|
||||
import { Seed } from "@app/lib/seed";
|
||||
import { config } from "@app/lib/config";
|
||||
import {
|
||||
formatNodeId,
|
||||
formatSeedHost,
|
||||
twemoji,
|
||||
extractHost,
|
||||
} from "@app/lib/utils";
|
||||
|
||||
import Clipboard from "@app/components/Clipboard.svelte";
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
import NotFound from "@app/components/NotFound.svelte";
|
||||
import Projects from "@app/views/seeds/View/Projects.svelte";
|
||||
import SeedAddress from "@app/views/seeds/View/SeedAddress.svelte";
|
||||
import { Project } from "@app/lib/project";
|
||||
import { Seed, defaultSeedPort } from "@app/lib/seed";
|
||||
import { formatSeedId, formatSeedHost, twemoji } from "@app/lib/utils";
|
||||
|
||||
export let hostAndPort: string;
|
||||
|
||||
const [host, port] = hostAndPort.includes(":")
|
||||
? hostAndPort.split(":")
|
||||
: [hostAndPort, defaultSeedPort];
|
||||
|
||||
const hostName = formatSeedHost(host);
|
||||
const seedHost: Host = { host, port: Number(port) };
|
||||
$: seedHost = extractHost(hostAndPort);
|
||||
$: hostName = formatSeedHost(seedHost.host);
|
||||
|
||||
const getProjectsAndStats = async (
|
||||
seed: Seed,
|
||||
@@ -28,7 +30,7 @@
|
||||
projects: ProjectInfo[];
|
||||
}> => {
|
||||
const stats = await seed.getStats();
|
||||
const projects = await Project.getProjects(seedHost, { perPage: 10 });
|
||||
const projects = await Project.getProjects(seed.addr, { perPage: 10 });
|
||||
return { stats, projects };
|
||||
};
|
||||
</script>
|
||||
@@ -81,7 +83,7 @@
|
||||
<title>{hostName}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#await Seed.lookup(host, Number(port))}
|
||||
{#await Seed.lookup(seedHost)}
|
||||
<main class="layout-centered">
|
||||
<Loading center />
|
||||
</main>
|
||||
@@ -99,17 +101,19 @@
|
||||
<div class="fields">
|
||||
<!-- Seed Address -->
|
||||
<div class="txt-highlight">Address</div>
|
||||
<SeedAddress {seed} port={seed.node.port} />
|
||||
<SeedAddress
|
||||
{seed}
|
||||
port={seed.node.port ?? config.seeds.defaultHttpdPort} />
|
||||
<!-- Seed ID -->
|
||||
<div class="txt-highlight">Seed ID</div>
|
||||
<div class="seed-label">
|
||||
{formatSeedId(seed.id)}
|
||||
{formatNodeId(seed.id)}
|
||||
<Clipboard small text={seed.id} />
|
||||
</div>
|
||||
<div class="layout-desktop" />
|
||||
<!-- API Port -->
|
||||
<div class="txt-highlight">API Port</div>
|
||||
<div>{port}</div>
|
||||
<div>{seed.addr.port}</div>
|
||||
<div class="layout-desktop" />
|
||||
<!-- API Version -->
|
||||
<div class="txt-highlight">Version</div>
|
||||
@@ -133,7 +137,7 @@
|
||||
{:catch}
|
||||
<div class="layout-centered">
|
||||
<NotFound
|
||||
title={host}
|
||||
title={seedHost.host}
|
||||
subtitle="Not able to query information from this seed." />
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import * as router from "@app/lib/router";
|
||||
import List from "@app/components/List.svelte";
|
||||
import Widget from "@app/views/projects/Widget.svelte";
|
||||
import { config } from "@app/lib/config";
|
||||
|
||||
export let seed: Seed;
|
||||
export let projects: proj.ProjectInfo[];
|
||||
@@ -39,9 +40,10 @@
|
||||
params: {
|
||||
view: { resource: "tree" },
|
||||
id: project.id,
|
||||
seed: seed.addr.port
|
||||
? `${seed.addr.host}:${seed.addr.port}`
|
||||
: seed.addr.host,
|
||||
seed:
|
||||
seed.addr.port === config.seeds.defaultHttpdPort
|
||||
? seed.addr.host
|
||||
: `${seed.addr.host}:${seed.addr.port}`,
|
||||
revision: project.head ?? undefined,
|
||||
hash: undefined,
|
||||
search: undefined,
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
expect,
|
||||
projectFixtureUrl,
|
||||
rid,
|
||||
ridPrefix,
|
||||
seedRemote,
|
||||
test,
|
||||
} from "@tests/support/fixtures.js";
|
||||
@@ -37,33 +36,43 @@ test("copy to clipboard", async ({ page, browserName, context }) => {
|
||||
const clipboardContent = await page.evaluate<string>(
|
||||
"navigator.clipboard.readText()",
|
||||
);
|
||||
expect(clipboardContent).toBe(`${ridPrefix}${rid}`);
|
||||
expect(clipboardContent).toBe(`${rid}`);
|
||||
}
|
||||
|
||||
// `rad clone` URL.
|
||||
{
|
||||
await page.getByText("Clone").click();
|
||||
await page
|
||||
.locator(`text=rad clone rad://0.0.0.0/${rid.substring(0, 6)}`)
|
||||
.hover();
|
||||
await page.locator(`text=rad clone ${rid.substring(0, 6)}`).hover();
|
||||
await page
|
||||
.locator(".clone-url-wrapper > span")
|
||||
.first()
|
||||
.locator(".clipboard")
|
||||
.click();
|
||||
await expectClipboard(`rad clone rad://0.0.0.0/${rid}`, page);
|
||||
await expectClipboard(`rad clone ${rid}`, page);
|
||||
}
|
||||
|
||||
// `git clone` URL.
|
||||
{
|
||||
await page.getByText("Clone").click();
|
||||
await page.locator(`text=https://0.0.0.0/${rid.substring(0, 6)}`).hover();
|
||||
await page
|
||||
.locator(
|
||||
`text=git clone https://0.0.0.0/${rid
|
||||
.replace("rad:", "")
|
||||
.substring(0, 10)}`,
|
||||
)
|
||||
.hover();
|
||||
await page
|
||||
.locator(".clone-url-wrapper > span")
|
||||
.last()
|
||||
.locator(".clipboard")
|
||||
.click();
|
||||
await expectClipboard(`https://0.0.0.0/${rid}.git`, page);
|
||||
await expectClipboard(
|
||||
`git clone https://0.0.0.0/${rid.replace(
|
||||
"rad:",
|
||||
"",
|
||||
)}.git source-browsing`,
|
||||
page,
|
||||
);
|
||||
}
|
||||
|
||||
await page.goto("/seeds/radicle.local");
|
||||
|
||||
+25
-53
@@ -6,7 +6,6 @@ import {
|
||||
expect,
|
||||
projectFixtureUrl,
|
||||
rid,
|
||||
ridPrefix,
|
||||
test,
|
||||
} from "@tests/support/fixtures.js";
|
||||
import { expectUrlPersistsReload } from "@tests/support/router";
|
||||
@@ -29,7 +28,7 @@ test("navigate to project", async ({ page }) => {
|
||||
// Header.
|
||||
{
|
||||
const name = page.locator("text=source-browsing");
|
||||
const id = page.locator(`text=${ridPrefix}${rid}`);
|
||||
const id = page.locator(`text=${rid}`);
|
||||
const description = page.locator(
|
||||
"text=Git repository for source browsing tests",
|
||||
);
|
||||
@@ -245,10 +244,10 @@ test("clone modal", async ({ page }) => {
|
||||
await page.goto(projectFixtureUrl);
|
||||
|
||||
await page.getByText("Clone").click();
|
||||
await expect(page.locator(`text=rad clone ${rid}`)).toBeVisible();
|
||||
await expect(
|
||||
page.locator(`text=rad clone rad://0.0.0.0/${rid}`),
|
||||
page.locator(`text=https://0.0.0.0/${rid.replace("rad:", "")}.git`),
|
||||
).toBeVisible();
|
||||
await expect(page.locator(`text=https://0.0.0.0/${rid}.git`)).toBeVisible();
|
||||
});
|
||||
|
||||
test("peer and branch switching", async ({ page }) => {
|
||||
@@ -257,18 +256,13 @@ test("peer and branch switching", async ({ page }) => {
|
||||
// Alice's peer.
|
||||
{
|
||||
await page.getByTitle("Change peer").click();
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${aliceRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`did:key:${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
|
||||
);
|
||||
} else {
|
||||
await page.locator("text=alice").click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText("alice delegate");
|
||||
}
|
||||
await page.locator(`text=${aliceRemote.substring(0, 6)}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`did:key:${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
|
||||
);
|
||||
await expect(
|
||||
page.locator(
|
||||
`text=source-browsing / ${aliceRemote.substring(
|
||||
`text=source-browsing / did:key:${aliceRemote.substring(
|
||||
0,
|
||||
6,
|
||||
)}…${aliceRemote.slice(-6)}`,
|
||||
@@ -326,32 +320,19 @@ test("peer and branch switching", async ({ page }) => {
|
||||
// Bob's peer.
|
||||
{
|
||||
await page.getByTitle("Change peer").click();
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${bobRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
);
|
||||
} else {
|
||||
await page.locator("text=bob").click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText("bob");
|
||||
}
|
||||
await page.locator(`text=${bobRemote.substring(0, 6)}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
);
|
||||
await expect(page.getByTitle("Change peer")).not.toHaveText("delegate");
|
||||
|
||||
// Default `main` branch.
|
||||
{
|
||||
if (process.env.HEARTWOOD) {
|
||||
await expect(page.getByTitle("Current branch")).toContainText(
|
||||
"main 1e0bb83",
|
||||
);
|
||||
await expectCounts({ commits: 9, contributors: 2 }, page);
|
||||
await expect(page.locator("text=1e0bb83 Update readme")).toBeVisible();
|
||||
} else {
|
||||
await expect(page.getByTitle("Current branch")).toContainText(
|
||||
"main 2b32f6f",
|
||||
);
|
||||
await expectCounts({ commits: 9, contributors: 2 }, page);
|
||||
await expect(page.locator("text=2b32f6f Update readme")).toBeVisible();
|
||||
}
|
||||
await expect(page.getByTitle("Current branch")).toContainText(
|
||||
"main 1e0bb83",
|
||||
);
|
||||
await expectCounts({ commits: 9, contributors: 2 }, page);
|
||||
await expect(page.locator("text=1e0bb83 Update readme")).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -360,11 +341,7 @@ test("only one modal can be open at a time", async ({ page }) => {
|
||||
await page.goto(projectFixtureUrl);
|
||||
|
||||
await page.getByTitle("Change peer").click();
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${aliceRemote.substring(0, 6)}`).click();
|
||||
} else {
|
||||
await page.locator(`text=alice ${aliceRemote.substring(0, 6)}`).click();
|
||||
}
|
||||
await page.locator(`text=${aliceRemote.substring(0, 6)}`).click();
|
||||
|
||||
await page.getByText("Clone").click();
|
||||
await expect(page.locator("text=Code font")).not.toBeVisible();
|
||||
@@ -381,11 +358,7 @@ test("only one modal can be open at a time", async ({ page }) => {
|
||||
await page.getByTitle("Change peer").click();
|
||||
await expect(page.locator("text=Code font")).not.toBeVisible();
|
||||
await expect(page.locator("text=Use the Radicle CLI")).not.toBeVisible();
|
||||
if (process.env.HEARTWOOD) {
|
||||
await expect(page.locator(`text=${bobRemote}`)).toBeVisible();
|
||||
} else {
|
||||
await expect(page.locator("text=bob hyyzz9")).toBeVisible();
|
||||
}
|
||||
await expect(page.locator(`text=${bobRemote.substring(0, 6)}`)).toBeVisible();
|
||||
await expect(page.locator("text=feature/branch")).not.toBeVisible();
|
||||
|
||||
await page.locator('button[name="Settings"]').click();
|
||||
@@ -398,7 +371,7 @@ test("only one modal can be open at a time", async ({ page }) => {
|
||||
test.describe("browser error handling", () => {
|
||||
test("error appears when folder can't be loaded", async ({ page }) => {
|
||||
await page.route(
|
||||
`**/v1/projects/${ridPrefix}${rid}/tree/${aliceMainHead}/markdown/`,
|
||||
`**/v1/projects/${rid}/tree/${aliceMainHead}/markdown/`,
|
||||
route => route.fulfill({ status: 500 }),
|
||||
);
|
||||
|
||||
@@ -413,7 +386,7 @@ test.describe("browser error handling", () => {
|
||||
});
|
||||
test("error appears when file can't be loaded", async ({ page }) => {
|
||||
await page.route(
|
||||
`**/v1/projects/${ridPrefix}${rid}/blob/${aliceMainHead}/.hidden`,
|
||||
`**/v1/projects/${rid}/blob/${aliceMainHead}/.hidden`,
|
||||
route => route.fulfill({ status: 500 }),
|
||||
);
|
||||
|
||||
@@ -423,9 +396,8 @@ test.describe("browser error handling", () => {
|
||||
await expect(page.locator("text=Not able to load file")).toBeVisible();
|
||||
});
|
||||
test("error appears when README can't be loaded", async ({ page }) => {
|
||||
await page.route(
|
||||
`**/v1/projects/${ridPrefix}${rid}/readme/${aliceMainHead}`,
|
||||
route => route.fulfill({ status: 500 }),
|
||||
await page.route(`**/v1/projects/${rid}/readme/${aliceMainHead}`, route =>
|
||||
route.fulfill({ status: 500 }),
|
||||
);
|
||||
|
||||
await page.goto(projectFixtureUrl);
|
||||
@@ -435,7 +407,7 @@ test.describe("browser error handling", () => {
|
||||
});
|
||||
test("error appears when navigating to missing file", async ({ page }) => {
|
||||
await page.route(
|
||||
`**/v1/projects/${ridPrefix}${rid}/blob/${aliceMainHead}/.hidden`,
|
||||
`**/v1/projects/${rid}/blob/${aliceMainHead}/.hidden`,
|
||||
route => route.fulfill({ status: 500 }),
|
||||
);
|
||||
|
||||
@@ -447,7 +419,7 @@ test.describe("browser error handling", () => {
|
||||
page,
|
||||
}) => {
|
||||
await page.route(
|
||||
`**/v1/projects/${ridPrefix}${rid}/blob/${aliceMainHead}/src/black-square.png`,
|
||||
`**/v1/projects/${rid}/blob/${aliceMainHead}/src/black-square.png`,
|
||||
route => route.fulfill({ status: 404 }),
|
||||
);
|
||||
|
||||
|
||||
@@ -5,18 +5,12 @@ import {
|
||||
bobRemote,
|
||||
} from "@tests/support/fixtures.js";
|
||||
|
||||
const modifiedFileFixture = `${projectFixtureUrl}/remotes/${bobRemote}/commits/${
|
||||
process.env.HEARTWOOD
|
||||
? "1e0bb83a89b63da815f2fc24e7ae3c5ceb30e0eb"
|
||||
: "2b32f6fe50090ebdb4cd7441e943330da3e6ff04"
|
||||
}`;
|
||||
const modifiedFileFixture = `${projectFixtureUrl}/remotes/${bobRemote}/commits/${"1e0bb83a89b63da815f2fc24e7ae3c5ceb30e0eb"}`;
|
||||
|
||||
test("navigation from commit list", async ({ page }) => {
|
||||
await page.goto(projectFixtureUrl);
|
||||
await page.getByTitle("Change peer").click();
|
||||
await page
|
||||
.locator(process.env.HEARTWOOD ? `text=${bobRemote}` : "text=bob hyyzz9")
|
||||
.click();
|
||||
await page.locator(`text=${bobRemote.substring(0, 6)}`).click();
|
||||
await page.locator('role=button[name="Commit count"]').click();
|
||||
|
||||
await page.locator("text=Update readme").click();
|
||||
@@ -35,13 +29,7 @@ test("relative timestamps", async ({ page }) => {
|
||||
});
|
||||
await page.goto(modifiedFileFixture);
|
||||
await expect(
|
||||
page.locator(
|
||||
`.commit header >> text=${
|
||||
process.env.HEARTWOOD
|
||||
? "Bob Belcher committed now"
|
||||
: "bob committed 22 hours ago"
|
||||
}`,
|
||||
),
|
||||
page.locator(`.commit header >> text=${"Bob Belcher committed now"}`),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -52,16 +40,9 @@ test("modified file", async ({ page }) => {
|
||||
{
|
||||
const header = page.locator(".commit header");
|
||||
await expect(header.locator("text=Update readme")).toBeVisible();
|
||||
if (!process.env.HEARTWOOD) {
|
||||
await expect(header.locator("text=Verified")).toBeVisible();
|
||||
await expect(
|
||||
header.locator("text=2b32f6fe50090ebdb4cd7441e943330da3e6ff04"),
|
||||
).toBeVisible();
|
||||
} else {
|
||||
await expect(
|
||||
header.locator("text=1e0bb83a89b63da815f2fc24e7ae3c5ceb30e0eb"),
|
||||
).toBeVisible();
|
||||
}
|
||||
await expect(
|
||||
header.locator("text=1e0bb83a89b63da815f2fc24e7ae3c5ceb30e0eb"),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
// Diff header.
|
||||
|
||||
@@ -13,15 +13,10 @@ test("peer and branch switching", async ({ page }) => {
|
||||
// Alice's peer.
|
||||
{
|
||||
await page.getByTitle("Change peer").click();
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${aliceRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`did:key:${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
|
||||
);
|
||||
} else {
|
||||
await page.locator("text=alice hybg18").click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText("alice delegate");
|
||||
}
|
||||
await page.locator(`text=${aliceRemote.substring(0, 6)}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`did:key:${aliceRemote.substring(0, 6)}…${aliceRemote.slice(-6)}`,
|
||||
);
|
||||
|
||||
await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
|
||||
await expect(
|
||||
@@ -66,20 +61,15 @@ test("peer and branch switching", async ({ page }) => {
|
||||
// Bob's peer.
|
||||
{
|
||||
await page.getByTitle("Change peer").click();
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${bobRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
);
|
||||
} else {
|
||||
await page.locator("text=bob hyyzz9").click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText("bob");
|
||||
}
|
||||
await page.locator(`text=${bobRemote.substring(0, 6)}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
);
|
||||
|
||||
await expect(page.getByText("Tuesday, December 20, 2022")).toBeVisible();
|
||||
await expect(
|
||||
page.locator(".commit-group-headers").first().locator(".commit-teaser"),
|
||||
).toHaveCount(process.env.HEARTWOOD ? 1 : 3);
|
||||
).toHaveCount(1);
|
||||
|
||||
await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
|
||||
await expect(
|
||||
@@ -88,13 +78,8 @@ test("peer and branch switching", async ({ page }) => {
|
||||
|
||||
await page.pause();
|
||||
const latestCommit = page.locator(".commit-teaser").first();
|
||||
if (process.env.HEARTWOOD) {
|
||||
await expect(latestCommit).toContainText("Update readme");
|
||||
await expect(latestCommit).toContainText("1e0bb83");
|
||||
} else {
|
||||
await expect(latestCommit).toContainText("Update readme");
|
||||
await expect(latestCommit).toContainText("2b32f6f");
|
||||
}
|
||||
await expect(latestCommit).toContainText("Update readme");
|
||||
await expect(latestCommit).toContainText("1e0bb83");
|
||||
|
||||
const earliestCommit = page.locator(".commit-teaser").last();
|
||||
await expect(earliestCommit).toContainText(
|
||||
@@ -104,30 +89,6 @@ test("peer and branch switching", async ({ page }) => {
|
||||
}
|
||||
});
|
||||
|
||||
test("verified badge", async ({ page }) => {
|
||||
await page.goto(projectFixtureUrl);
|
||||
await page.locator('role=button[name="Commit count"]').click();
|
||||
|
||||
await page.getByTitle("Change peer").click();
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${bobRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
);
|
||||
} else {
|
||||
await page.locator("text=bob hyyzz9").click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText("bob");
|
||||
// not applicable to heartwood?
|
||||
await page.locator("text=Verified").hover();
|
||||
await expect(
|
||||
page.locator(
|
||||
"text=This commit was signed with the committer's radicle key.",
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(page.locator(`text=bob committed ${bobRemote}`)).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test("relative timestamps", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.initializeTestStubs = () => {
|
||||
@@ -143,22 +104,13 @@ test("relative timestamps", async ({ page }) => {
|
||||
await page.locator('role=button[name="Commit count"]').click();
|
||||
|
||||
await page.getByTitle("Change peer").click();
|
||||
if (process.env.HEARTWOOD) {
|
||||
await page.locator(`text=${bobRemote}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
);
|
||||
const latestCommit = page.locator(".commit-teaser").first();
|
||||
await expect(latestCommit).toContainText("Bob Belcher committed now");
|
||||
await expect(latestCommit).toContainText("1e0bb83");
|
||||
} else {
|
||||
await page.locator("text=bob hyyzz9").click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText("bob");
|
||||
const latestCommit = page.locator(".commit-teaser").first();
|
||||
await expect(latestCommit).toContainText("bob committed 22 hours ago");
|
||||
await expect(latestCommit).toContainText("2b32f6f");
|
||||
}
|
||||
|
||||
await page.locator(`text=${bobRemote.substring(0, 6)}`).click();
|
||||
await expect(page.getByTitle("Change peer")).toHaveText(
|
||||
`did:key:${bobRemote.substring(0, 6)}…${bobRemote.slice(-6)}`,
|
||||
);
|
||||
const latestCommit = page.locator(".commit-teaser").first();
|
||||
await expect(latestCommit).toContainText("Bob Belcher committed now");
|
||||
await expect(latestCommit).toContainText("1e0bb83");
|
||||
const earliestCommit = page.locator(".commit-teaser").last();
|
||||
await expect(earliestCommit).toContainText(
|
||||
"Alice Liddell committed last month",
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
test,
|
||||
expect,
|
||||
rid,
|
||||
ridPrefix,
|
||||
projectFixtureUrl,
|
||||
} from "@tests/support/fixtures.js";
|
||||
|
||||
@@ -10,11 +9,11 @@ test("navigate to existing project", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const searchInput = page.getByPlaceholder("Search a name…");
|
||||
await searchInput.click();
|
||||
await searchInput.fill(`${ridPrefix}${rid}`);
|
||||
await searchInput.fill(`${rid}`);
|
||||
await searchInput.press("Enter");
|
||||
|
||||
await expect(page).toHaveURL(`${projectFixtureUrl}/tree`);
|
||||
await expect(searchInput).not.toHaveValue(`${ridPrefix}${rid}`);
|
||||
await expect(searchInput).not.toHaveValue(`${rid}`);
|
||||
});
|
||||
|
||||
test("navigate to a project that does not exist", async ({ page }) => {
|
||||
@@ -22,7 +21,7 @@ test("navigate to a project that does not exist", async ({ page }) => {
|
||||
const searchInput = page.getByPlaceholder("Search a name…");
|
||||
await searchInput.click();
|
||||
|
||||
const nonExistantId = `${ridPrefix}:zt${rid.substring(2)}`;
|
||||
const nonExistantId = "rad:zAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
await searchInput.fill(nonExistantId);
|
||||
await searchInput.press("Enter");
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
aliceMainHead,
|
||||
expect,
|
||||
rid,
|
||||
ridPrefix,
|
||||
seedPort,
|
||||
seedRemote,
|
||||
seedVersion,
|
||||
@@ -44,8 +43,8 @@ test("seed projects", async ({ page }) => {
|
||||
|
||||
// Show project ID on hover.
|
||||
{
|
||||
await expect(project.locator(`text=${ridPrefix}${rid}`)).not.toBeVisible();
|
||||
await expect(project.locator(`text=${rid}`)).not.toBeVisible();
|
||||
await project.hover();
|
||||
await expect(project.locator(`text=${ridPrefix}${rid}`)).toBeVisible();
|
||||
await expect(project.locator(`text=${rid}`)).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
+17
-21
@@ -49,6 +49,9 @@ export const test = base.extend<{
|
||||
window.APP_CONFIG = {
|
||||
reactions: [],
|
||||
seeds: {
|
||||
defaultHttpdPort: 8080,
|
||||
defaultHttpdScheme: "http",
|
||||
defaultNodePort: 8776,
|
||||
pinned: [{ host: "0.0.0.0", emoji: "🚀" }],
|
||||
},
|
||||
projects: { pinned: [] },
|
||||
@@ -124,14 +127,13 @@ function log(text: string, label: string, outputLog: Stream.Writable) {
|
||||
}
|
||||
}
|
||||
|
||||
export const appConfigWithFixture = process.env.HEARTWOOD
|
||||
? configFixtureHeartwood
|
||||
: configFixture;
|
||||
|
||||
export function configFixture() {
|
||||
window.APP_CONFIG = {
|
||||
reactions: [],
|
||||
seeds: {
|
||||
defaultHttpdPort: 8080,
|
||||
defaultHttpdScheme: "http",
|
||||
defaultNodePort: 8776,
|
||||
pinned: [{ host: "0.0.0.0", emoji: "🚀" }],
|
||||
},
|
||||
projects: {
|
||||
@@ -146,10 +148,13 @@ export function configFixture() {
|
||||
};
|
||||
}
|
||||
|
||||
export function configFixtureHeartwood() {
|
||||
export function appConfigWithFixture() {
|
||||
window.APP_CONFIG = {
|
||||
reactions: [],
|
||||
seeds: {
|
||||
defaultHttpdPort: 8080,
|
||||
defaultHttpdScheme: "http",
|
||||
defaultNodePort: 8776,
|
||||
pinned: [{ host: "0.0.0.0", emoji: "🚀" }],
|
||||
},
|
||||
projects: {
|
||||
@@ -165,19 +170,10 @@ export function configFixtureHeartwood() {
|
||||
}
|
||||
|
||||
export const aliceMainHead = "fcc929424b82984b7cbff9c01d2e20d9b1249842";
|
||||
export const aliceRemote = process.env.HEARTWOOD
|
||||
? "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
: "hybg18bc4cu8z9xtj44skxperfdpxpp1wp8zygyzti5kfiggdizfxy";
|
||||
export const bobRemote = process.env.HEARTWOOD
|
||||
? "z6MksMTThc1aDU2Ztc43jJUivuyBLNWiLsDf4X65rABe7HbA"
|
||||
: "hyyzz9w4ffg16zftjki3enajm4mkqkayb5ch1p6ns3f83np1hqkrp6";
|
||||
export const rid = process.env.HEARTWOOD
|
||||
? "zKtT7DmF9H34KkvcKj9PHW19WzjT"
|
||||
: "hnrkdi8be7n4hhqoz9rpzrgd68u9dr3zsxgmy";
|
||||
export const ridPrefix = process.env.HEARTWOOD ? "rad:" : "rad:git:";
|
||||
export const projectFixtureUrl = `/seeds/0.0.0.0/${ridPrefix}${rid}`;
|
||||
export const seedPort = process.env.HEARTWOOD ? 8080 : "8777";
|
||||
export const seedVersion = process.env.HEARTWOOD ? "0.1.0" : "0.2.0";
|
||||
export const seedRemote = process.env.HEARTWOOD
|
||||
? "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"
|
||||
: "hybuytx44z9cfsm5739wecia9j4b7expgc15qkazph59szp57m4d3o";
|
||||
export const aliceRemote = "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi";
|
||||
export const bobRemote = "z6MksMTThc1aDU2Ztc43jJUivuyBLNWiLsDf4X65rABe7HbA";
|
||||
export const rid = "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT";
|
||||
export const projectFixtureUrl = `/seeds/0.0.0.0/${rid}`;
|
||||
export const seedPort = 8080;
|
||||
export const seedVersion = "0.1.0";
|
||||
export const seedRemote = "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8";
|
||||
|
||||
@@ -11,28 +11,20 @@ export default async function globalSetup(_config: FullConfig): Promise<void> {
|
||||
async function assertHttpApiRunning(): Promise<void> {
|
||||
const notRunningMessage =
|
||||
"The http-api server with test fixtures needs to be running.\n" +
|
||||
(process.env.HEARTWOOD
|
||||
? "👉 You can start it with `./scripts/run-httpd-with-fixtures`\n"
|
||||
: "👉 You can start it with `./scripts/run-http-api-with-fixtures`\n");
|
||||
"👉 You can start it with `./scripts/run-httpd-with-fixtures`\n";
|
||||
|
||||
let peerId: string | undefined = undefined;
|
||||
let nodeId: string | undefined = undefined;
|
||||
|
||||
try {
|
||||
if (process.env.HEARTWOOD) {
|
||||
const response = await fetch(`http://0.0.0.0:${seedPort}/api`);
|
||||
const data = await response.json();
|
||||
peerId = data.node.id;
|
||||
} else {
|
||||
const response = await fetch(`http://0.0.0.0:${seedPort}`);
|
||||
const data = await response.json();
|
||||
peerId = data.peer.id;
|
||||
}
|
||||
const response = await fetch(`http://0.0.0.0:${seedPort}/api`);
|
||||
const data = await response.json();
|
||||
nodeId = data.node.id;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw new Error(notRunningMessage);
|
||||
}
|
||||
|
||||
if (peerId !== seedRemote) {
|
||||
if (nodeId !== seedRemote) {
|
||||
const wrongSeedMessage = `The server on port ${seedPort} doesn't have the right fixtures.\n`;
|
||||
throw new Error(wrongSeedMessage + notRunningMessage);
|
||||
}
|
||||
|
||||
+51
-26
@@ -9,6 +9,15 @@ describe("Format functions", () => {
|
||||
expect(utils.formatLocationHash(hash)).toEqual(expected);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
|
||||
expected: "rad:zKtT7D…19WzjT",
|
||||
},
|
||||
])("formatRepositoryId $id => $expected", ({ id, expected }) => {
|
||||
expect(utils.formatRepositoryId(id)).toEqual(expected);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
id: "did:key:z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
|
||||
@@ -22,22 +31,17 @@ describe("Format functions", () => {
|
||||
expect(utils.formatNodeId(id)).toEqual(expected);
|
||||
});
|
||||
|
||||
test("formatRadicleId", () => {
|
||||
if (process.env.HEARTWOOD) {
|
||||
expect(utils.formatRadicleId("rad:zKtT7DmF9H34KkvcKj9PHW19WzjT")).toEqual(
|
||||
"rad:zKtT7D…19WzjT",
|
||||
);
|
||||
} else {
|
||||
expect(
|
||||
utils.formatRadicleId("rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o"),
|
||||
).toEqual("rad:git:hnrkem…d84c9o");
|
||||
}
|
||||
});
|
||||
|
||||
test("formatRadicleId throw when wrong ID", () => {
|
||||
expect(() =>
|
||||
utils.formatRadicleId("hnrkemobagsicpf9sr95o3g551otspcd84c9o"),
|
||||
).toThrow();
|
||||
test.each([
|
||||
{
|
||||
id: "rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5",
|
||||
expected: "rad:z4V1sj…p4DtH5",
|
||||
},
|
||||
{
|
||||
id: "z4V1sjrXqjvFdnCUbxPFqd5p4DtH5",
|
||||
expected: "rad:z4V1sj…p4DtH5",
|
||||
},
|
||||
])("formatRepositoryId $id => $expected", ({ id, expected }) => {
|
||||
expect(utils.formatRepositoryId(id)).toEqual(expected);
|
||||
});
|
||||
|
||||
test.each([
|
||||
@@ -67,19 +71,40 @@ describe("String Assertions", () => {
|
||||
});
|
||||
|
||||
test.each([
|
||||
process.env.HEARTWOOD
|
||||
? { id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", expected: true }
|
||||
: { id: "rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o", expected: true },
|
||||
{ id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", expected: true },
|
||||
{ id: "z2H9aHDurxd8Uvx2jsvW4e5mamy5S", expected: true },
|
||||
{ id: "rad:BBBBBBBBBBBBBBBBBBBBBBBBBBBB", expected: false },
|
||||
{ id: "0x1234567890123456789012345678901234567890", expected: false },
|
||||
])("isRadicleId $id => $expected", ({ id, expected }) => {
|
||||
expect(utils.isRadicleId(id)).toEqual(expected);
|
||||
{
|
||||
id: "did:key:z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
id: "z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx",
|
||||
expected: false,
|
||||
},
|
||||
])("isRepositoryId $id => $expected", ({ id, expected }) => {
|
||||
expect(utils.isRepositoryId(id)).toEqual(expected);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ id: "hnrkj4c35uoyceb3d1dsscx8qq55cikrd1aio", expected: true },
|
||||
{
|
||||
id: "did:key:z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
id: "z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
id: "did:key:CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
|
||||
expected: false,
|
||||
},
|
||||
{ id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", expected: false },
|
||||
{ id: "z2H9aHDurxd8Uvx2jsvW4e5mamy5S", expected: false },
|
||||
{ id: "0x1234567890123456789012345678901234567890", expected: false },
|
||||
])("isPeerId $id => $expected", ({ id, expected }) => {
|
||||
expect(utils.isPeerId(id)).toEqual(expected);
|
||||
])("isNodeId $id => $expected", ({ id, expected }) => {
|
||||
expect(utils.isNodeId(id)).toEqual(expected);
|
||||
});
|
||||
|
||||
test.each([
|
||||
@@ -131,8 +156,8 @@ describe("Parse Functions", () => {
|
||||
pubkey: "z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
|
||||
},
|
||||
},
|
||||
])("parseNid", ({ input, expected }) => {
|
||||
expect(utils.parseNid(input)).toEqual(expected);
|
||||
])("parseNodeId", ({ input, expected }) => {
|
||||
expect(utils.parseNodeId(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ function defineConstants() {
|
||||
const constants = {
|
||||
VITEST: process.env.VITEST !== undefined,
|
||||
PLAYWRIGHT: process.env.PLAYWRIGHT_TEST_BASE_URL !== undefined,
|
||||
HEARTWOOD: process.env.HEARTWOOD !== undefined,
|
||||
};
|
||||
|
||||
// Don't overwrite HASH_ROUTING in Playwright tests, so we can control it
|
||||
|
||||
Reference in New Issue
Block a user