Add support for Heartwood

Co-authored-by: Rūdolfs Ošiņš <rudolfs@osins.org>
Signed-off-by: xphoniex <dj.2dixx@gmail.com>
Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
xphoniex
2023-01-04 11:20:14 +01:00
committed by Rūdolfs Ošiņš
co-authored by Rūdolfs Ošiņš
parent adc159a71e
commit f72405a3e4
36 changed files with 561 additions and 182 deletions
+9 -2
View File
@@ -12,7 +12,7 @@ jobs:
continue-on-error: true
strategy:
matrix:
browser: [chromium, firefox, visual]
browser: [chromium, firefox, visual, heartwood]
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
@@ -59,12 +59,19 @@ jobs:
run: npx playwright install-deps
- name: Start http-api test server
run: ./scripts/run-http-api-with-fixtures --non-interactive --detach
run: |
if [ ${{ matrix.browser }} = "heartwood" ]; then
./scripts/run-httpd-with-fixtures --non-interactive --detach
else
./scripts/run-http-api-with-fixtures --non-interactive --detach
fi
- 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
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
PASSPHRASE=asdf
SEED="0.0.0.0:8080"
REPO_ROOT=$(git rev-parse --show-toplevel)
ID=$(echo $RANDOM | md5sum | head -c 8)
BASE_PATH=$REPO_ROOT/tests/tmp/create-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
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 $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
RAD_HOME=$PALM_RAD_HOME RAD_PASSPHRASE=$PASSPHRASE rad auth
GIT_AUTHOR_NAME="Alice Liddell"
GIT_AUTHOR_EMAIL="alice@radicle.xyz"
GIT_COMMITTER_NAME="Alice Liddell"
GIT_COMMITTER_EMAIL="alice@radicle.xyz"
RAD_DEBUG=1 RAD_HOME=$ALICE_RAD_HOME RAD_PASSPHRASE=$PASSPHRASE 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_HOME=$ALICE_RAD_HOME RAD_PASSPHRASE=$PASSPHRASE rad init --name "source-browsing" --description "Git repository for source browsing tests" --default-branch "main" --no-confirm
PROJECT_ID=$(RAD_HOME=$ALICE_RAD_HOME RAD_PASSPHRASE=$PASSPHRASE rad .)
cd $BASE_PATH
tar -cjf palm.tar.bz2 --exclude "post-receive" --exclude "pre-receive" -C $ALICE_RAD_HOME .
+14 -18
View File
@@ -10,8 +10,7 @@ PASSPHRASE=asdf
CONTAINER_NAME=radicle-http-api-with-fixtures
HTTP_API_BINARY=radicle-http-api
show_usage()
{
show_usage() {
echo
echo "Starts a http-api backend with test fixtures."
echo
@@ -26,8 +25,7 @@ show_usage()
echo
}
prompt_workspace_removal()
{
prompt_workspace_removal() {
echo "This will irrevocably destroy the following directories:"
echo
echo $WORKSPACE
@@ -35,19 +33,18 @@ prompt_workspace_removal()
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
;;
[yY][eE][sS])
rm -rf $WORKSPACE
echo "Done"
;;
*)
echo "Ok, I won't touch your data."
exit
;;
esac
}
prepare_workspace()
{
prepare_workspace() {
echo
echo "Unpacking fixture $FIXTURE"
mkdir -p $WORKSPACE
@@ -94,13 +91,13 @@ DETACH=false
while [ $# -ne 0 ]; do
case "$1" in
--binary|-b)
--binary | -b)
BINARY=true
;;
--detach|-d)
--detach | -d)
DETACH=true
;;
--non-interactive|-n)
--non-interactive | -n)
NON_INTERACTIVE=true
;;
*)
@@ -120,7 +117,6 @@ else
prepare_workspace
fi
if [ "$BINARY" = true ]; then
run_binary
else
+132
View File
@@ -0,0 +1,132 @@
#!/bin/sh
set -e
REV=1f55d7a32750b3e63c56aad00370411b90420a29
REPO_ROOT=$(git rev-parse --show-toplevel)
FIXTURE=$REPO_ROOT/tests/fixtures/seeds/palm-heartwood.tar.bz2
WORKSPACE=$REPO_ROOT/tests/tmp/palm
PASSPHRASE=asdf
CONTAINER_NAME=radicle-httpd-with-fixtures
HTTP_API_BINARY=radicle-httpd
show_usage() {
echo
echo "Starts a radicle-httpd backend with test fixtures."
echo
echo "USAGE:"
echo " run-httpd-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
if [ "$CI" = true ]; then
sudo chown -R 0:0 $WORKSPACE
fi
}
run_docker() {
echo "Starting docker at container $CONTAINER_NAME at $REV"
echo " RAD_HOME=$WORKSPACE RAD_PASSPHRASE=$PASSPHRASE radicle-http"
echo
exec docker run \
--init \
--publish 8080:8080 \
--rm \
--name $CONTAINER_NAME \
--volume $WORKSPACE:/app/radicle \
"$@" \
--env "RAD_HOME=/app/radicle" \
--env "RAD_PASSPHRASE=$PASSPHRASE" \
"gcr.io/radicle-services/radicle-httpd:$REV"
}
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/heartwood"
echo
exit 1
fi
echo
echo "Starting $HTTP_API_BINARY"
echo " RAD_HOME=$WORKSPACE $HTTP_API_BINARY"
echo
RAD_HOME=$WORKSPACE RAD_PASSPHRASE=$PASSPHRASE $HTTP_API_BINARY
}
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
+6 -2
View File
@@ -6,7 +6,11 @@
import Address from "@app/components/Address.svelte";
import { Profile, ProfileType } from "@app/lib/profile";
import { formatRadicleId, formatTimestamp } from "@app/lib/utils";
import {
formatRadicleId,
formatSeedId,
formatTimestamp,
} from "@app/lib/utils";
export let noAvatar = false;
export let author: Author;
@@ -67,7 +71,7 @@
</span>
{:else}
<span class="highlight">
{formatRadicleId(author.id)}
{window.HEARTWOOD ? formatSeedId(author.id) : formatRadicleId(author.id)}
</span>
{/if}
<span class="caption">&nbsp;{caption}&nbsp;</span>
+8 -4
View File
@@ -20,17 +20,19 @@
const validationExamples: Record<string, string> = {
URL: "https://acme.xyz/",
URN: "eip155:1:0xd1bb21bd5a432d2919c82bcefe1bc7f8cc9207d9",
ID: "eip155:1:0xd1bb21bd5a432d2919c82bcefe1bc7f8cc9207d9",
handle: "acme",
id: "hydkkcf6k9be5fuszdhpqbctu3q3fuwagj874wx2puia8ti8coygh1",
identity: "rad:git:hnrkqdpm9ub19oc8dccx44echy75hzfsezyio",
identity: window.HEARTWOOD
? "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT"
: "rad:git:hnrkqdpm9ub19oc8dccx44echy75hzfsezyio",
domain: "seed.acme.xyz",
address: "0x17a8c096733BD5F87aD43D7A2A4d1C42ab8A2A70",
};
const validationTypes: { [index: string]: RegExp } = {
URL: /^(https:\/\/|http:\/\/|ipfs:\/\/)\S+/,
URN: /^[a-z]+:[a-zA-Z0-9:-]{1,64}$/,
ID: /^z[a-z]+:[a-zA-Z0-9:-]{1,64}$/,
// Github
// Username may only contain alphanumeric characters or hyphens.
// Username cannot have multiple consecutive hyphens.
@@ -43,7 +45,9 @@
handle: /^[a-zA-Z0-9-_]{1,39}$/,
address: /^0x[a-zA-Z0-9]{40}$/,
id: /^[a-z0-9]+$/,
identity: /^rad:git:[a-z0-9]{37}$/,
identity: window.HEARTWOOD
? /^rad:[a-z0-9]{28}$/
: /^rad:git:[a-z0-9]{37}$/,
domain: /^[^/:$!_;,@#]+\.[a-z]{2,}$/,
};
</script>
+2
View File
@@ -10,6 +10,8 @@ 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 -1
View File
@@ -13,7 +13,11 @@ export class Request {
constructor(path: string, api: Host) {
this.port = api.port || defaultSeedPort;
this.base = api.host;
if (window.HEARTWOOD) {
this.base = api.host + "/api";
} else {
this.base = api.host;
}
this.path = path.startsWith("/") ? path.slice(1) : path;
this.protocol = api.host === "0.0.0.0" ? "http://" : "https://";
}
+1
View File
@@ -89,6 +89,7 @@ export interface Diff {
moved: string[];
copied: string[];
modified: FileDiff[];
stats: DiffStats;
}
export interface DiffStats {
+20 -9
View File
@@ -12,7 +12,7 @@ export interface IIssue {
author: Author;
title: string;
state: State;
comment: Comment;
comment: Comment; // TODO: Remove this after we have migrated to Heartwood.
discussion: Thread[];
tags: Tag[];
timestamp: number;
@@ -32,7 +32,8 @@ export interface Comment<R = null> {
body: string;
reactions: Record<string, number>;
timestamp: number;
replies: R;
replies: R; // TODO: Remove this after we have migrated to Heartwood.
replyTo: R;
}
export type Thread = Comment<Comment[]>;
@@ -57,7 +58,7 @@ export class Issue {
author: Author;
title: string;
state: State;
comment: Comment;
comment: Comment; // TODO: Remove this after we have migrated to Heartwood.
discussion: Thread[];
tags: Tag[];
timestamp: number;
@@ -67,18 +68,28 @@ export class Issue {
this.author = issue.author;
this.title = issue.title;
this.state = issue.state;
this.comment = issue.comment;
this.comment = issue.comment; // TODO: Remove this after we have migrated to Heartwood.
this.discussion = issue.discussion;
this.tags = issue.tags;
this.timestamp = issue.timestamp;
if (window.HEARTWOOD) {
this.timestamp = issue.discussion[0].timestamp;
} else {
this.timestamp = issue.timestamp;
}
}
// Counts the amount of comments and replies in a discussion
countComments(): number {
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);
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);
}
}
static async getIssues(id: string, host: Host): Promise<Issue[]> {
+10 -4
View File
@@ -100,10 +100,16 @@ export class Patch implements IPatch {
// Counts the amount of comments and replies in a discussion
countComments(rev: number): number {
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);
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) {
+73 -49
View File
@@ -12,6 +12,7 @@ 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";
@@ -40,38 +41,43 @@ export interface ProjectInfo {
name: string;
description: string;
defaultBranch: string;
delegates: Delegate[];
remotes: PeerId[];
delegates: Delegate[]; // TODO: Remove this after we have migrated to Heartwood.
remotes: PeerId[]; // TODO: Remove this after we have migrated to Heartwood.
patches?: number;
issues?: number;
}
export interface Tree {
path: string;
info: EntryInfo;
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";
export interface Stats {
commits: number;
contributors: number;
}
export enum ObjectType {
Blob = "BLOB",
Tree = "TREE",
}
// TODO: Remove this after we have migrated to Heartwood.
export interface EntryInfo {
name: string;
objectType: ObjectType;
objectType: Kind;
lastCommit: CommitHeader;
}
export interface Entry {
path: string;
info: EntryInfo;
info: EntryInfo; // TODO: Remove this after we have migrated to Heartwood.
name: string;
kind: Kind;
lastCommit: CommitHeader;
}
export interface Blob {
@@ -79,7 +85,10 @@ export interface Blob {
html?: boolean;
content: string;
path: string;
info: EntryInfo;
info: EntryInfo; // TODO: Remove this after we have migrated to Heartwood.
name: string;
kind: Kind;
lastCommit: CommitHeader;
}
export interface Remote {
@@ -144,8 +153,8 @@ export class Project implements ProjectInfo {
name: string;
description: string;
defaultBranch: string;
delegates: Delegate[];
remotes: PeerId[];
delegates: Delegate[]; // TODO: Remove this after we have migrated to Heartwood.
remotes: PeerId[]; // TODO: Remove this after we have migrated to Heartwood.
seed: Seed;
peers: Peer[];
branches: Branches;
@@ -167,8 +176,8 @@ export class Project implements ProjectInfo {
this.name = info.name;
this.description = info.description;
this.defaultBranch = info.defaultBranch;
this.delegates = info.delegates;
this.remotes = info.remotes;
this.delegates = info.delegates; // TODO: Remove this after we have migrated to Heartwood.
this.remotes = info.remotes; // TODO: Remove this after we have migrated to Heartwood.
this.seed = seed;
this.peers = peers;
this.branches = branches;
@@ -192,10 +201,14 @@ export class Project implements ProjectInfo {
}
static async getInfo(nameOrId: string, host: Host): Promise<ProjectInfo> {
const result = await new Request(`projects/${nameOrId}`, host).get();
result["id"] = result["urn"];
delete result["urn"];
return result;
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;
}
}
static async getProjects(
@@ -209,13 +222,17 @@ export class Project implements ProjectInfo {
"per-page": opts?.perPage,
page: opts?.page,
};
const results = await new Request("projects", host).get(params);
results.forEach((result: { [x: string]: any }) => {
result["id"] = result["urn"];
delete result["urn"];
});
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 results;
}
}
static async getDelegateProjects(
@@ -280,28 +297,31 @@ export class Project implements ProjectInfo {
`projects/${this.id}/commits/${commit}`,
this.seed.addr,
).get();
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 (!window.HEARTWOOD) {
result.stats["insertions"] = result.stats["additions"];
delete result.stats["additions"];
result.diff["added"] = result.diff["created"];
delete result.diff["created"];
if (line["lineNumNew"]) {
line["lineNoNew"] = line["lineNumNew"];
delete line["lineNumNew"];
}
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["lineNum"]) {
line["lineNo"] = line["lineNum"];
delete line["lineNum"];
if (line["lineNumNew"]) {
line["lineNoNew"] = line["lineNumNew"];
delete line["lineNumNew"];
}
if (line["lineNum"]) {
line["lineNo"] = line["lineNum"];
delete line["lineNum"];
}
}
}
}
@@ -364,12 +384,16 @@ export class Project implements ProjectInfo {
const info = await Project.getInfo(id, seed.addr);
id = isRadicleId(id) ? id : info.id;
// Older versions of http-api don't include the ID.
if (!info.id) info.id = id;
let peers: Peer[] = [];
const peers: Peer[] = info.delegates
? await Project.getRemotes(id, seed.addr)
: [];
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) : [];
}
let remote: Remote = {
heads: info.head ? { [info.defaultBranch]: info.head } : {},
+4 -16
View File
@@ -90,18 +90,8 @@ export async function getRegistration(
getText(resolver, "com.github"),
]);
const [
address,
avatar,
url,
id,
seedId,
seedHost,
seedGit,
seedApi,
twitter,
github,
] = meta.filter(isFulfilled).map(r => (r.value ? r.value : undefined));
const [address, avatar, url, id, seedId, seedHost, seedApi, twitter, github] =
meta.filter(isFulfilled).map(r => (r.value ? r.value : undefined));
const profile: EnsProfile = {
name,
@@ -119,7 +109,6 @@ export async function getRegistration(
profile.seed = new Seed({
host: seedHost,
id: seedId,
git: seedGit,
addr: seedApi,
});
} catch (e: any) {
@@ -157,10 +146,9 @@ export async function getSeed(
return null;
}
const [id, host, git, api] = await Promise.all([
const [id, host, api] = await Promise.all([
getText(resolver, "eth.radicle.seed.id"),
getText(resolver, "eth.radicle.seed.host"),
getText(resolver, "eth.radicle.seed.git"),
getText(resolver, "eth.radicle.seed.api"),
]);
@@ -170,7 +158,7 @@ export async function getSeed(
}
try {
return new Seed({ host, id, git, addr: api });
return new Seed({ host, id, addr: api });
} catch (e: any) {
console.debug(e, host, id);
return new InvalidSeed(id, host);
+36 -21
View File
@@ -23,15 +23,16 @@ export class InvalidSeed {
}
}
export const defaultSeedPort = 8777;
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 };
git: { host: string; port: number | null }; // TODO: Remove this after we have migrated to Heartwood.
node: { host: string; id: string; port: number };
version?: string;
@@ -40,17 +41,21 @@ export class Seed {
constructor(seed: {
host: string;
id: string;
git?: string | null;
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}`);
assert(/^[a-z0-9]+$/.test(seed.id), `invalid seed id ${seed.id}`);
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}`);
}
let _seed = null;
let _git = null;
let _git = null; // TODO: Remove this after we have migrated to Heartwood.
let _seedPort: number | null = defaultSeedPort;
let _gitPort: number | null = defaultGitPort;
let _gitPort: number | null = defaultGitPort; // TODO: Remove this after we have migrated to Heartwood.
if (seed.addr) {
try {
@@ -73,15 +78,17 @@ export class Seed {
assert(isDomain(_seed), `invalid seed host ${_seed}`);
}
if (seed.git) {
try {
const url = new URL(seed.git);
_git = url.hostname;
_gitPort = url.port ? Number(url.port) : null;
} catch {
_git = seed.git;
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}`);
}
assert(isDomain(_git), `invalid seed git host ${_git}`);
}
this.emoji = getSeedEmoji(seed.host);
@@ -89,10 +96,10 @@ export class Seed {
// The `_seed` being more specific takes
// precedence over the `host`, if available.
_seed = _seed ?? seed.host;
_git = _git ?? 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 };
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 };
if (seed.version) {
@@ -121,10 +128,14 @@ export class Seed {
? await proj.Project.getDelegateProjects(id, this.addr, { perPage })
: await proj.Project.getProjects(this.addr, { perPage });
return result.map((project: proj.ProjectInfo) => ({
...project,
id: project.id,
}));
if (window.HEARTWOOD) {
return result;
} else {
return result.map((project: proj.ProjectInfo) => ({
...project,
id: project.id,
}));
}
}
async getStats(): Promise<{
@@ -135,7 +146,11 @@ export class Seed {
}
static async getNode(host: Host): Promise<{ id: string }> {
return new Request("/peer", host).get();
if (window.HEARTWOOD) {
return new Request("/node", host).get();
} else {
return new Request("/peer", host).get();
}
}
static async getInfo(host: Host): Promise<{ version: string }> {
+15 -3
View File
@@ -105,7 +105,11 @@ export function formatSeedId(id: string): string {
export function formatRadicleId(id: string): string {
assert(isRadicleId(id));
return id.substring(0, 14) + "…" + id.substring(id.length - 6, id.length);
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);
}
}
export function formatBalance(n: BigNumber, decimals?: number): string {
@@ -245,7 +249,11 @@ export const formatTimestamp = (
// Check whether the input is a Radicle ID.
export function isRadicleId(input: string): boolean {
return /^rad:[a-z]+:[a-zA-Z0-9]+$/.test(input);
if (window.HEARTWOOD) {
return /^rad:[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.
@@ -295,7 +303,11 @@ export function formatName(input: string, wallet: Wallet): string {
// Parse a Radicle Id.
export function parseRadicleId(id: string): string {
return id.replace(/^rad:[a-z]+:/, "");
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
+2 -2
View File
@@ -18,7 +18,7 @@
export let line: string | undefined = undefined;
const fileExtension = blob.path.split(".").pop() ?? "";
const lastCommit = blob.info.lastCommit;
const lastCommit = window.HEARTWOOD ? blob.lastCommit : blob.info.lastCommit;
const parentDir = blob.path
.match(/^.*\/|/)
?.values()
@@ -213,7 +213,7 @@
<span class="file-name">
<span class="txt-faded">{parentDir}</span>
&#8203;
<span>{blob.info.name}</span>
<span>{window.HEARTWOOD ? blob.name : blob.info.name}</span>
</span>
<div class="right">
{#if isMarkdown}
+4 -1
View File
@@ -73,5 +73,8 @@
{/if}
</div>
</header>
<Changeset stats={commit.stats} diff={commit.diff} on:browse={onBrowse} />
<Changeset
stats={window.HEARTWOOD ? commit.diff.stats : commit.stats}
diff={commit.diff}
on:browse={onBrowse} />
</div>
@@ -94,7 +94,7 @@
<CommitAuthorship {commit} noDelegate />
</div>
<div class="column-right">
{#if commit.context.committer}
{#if !window.HEARTWOOD && commit.context.committer}
<div class="layout-desktop">
<CommitVerifiedBadge {commit} />
</div>
+4 -1
View File
@@ -98,9 +98,12 @@
{revision}
on:branchChanged={event => updateRevision(event.detail)} />
{#if seed.git.host}
{#if window.HEARTWOOD && seed.addr.host}
<CloneButton seedHost={seed.addr.host} {id} />
{:else if seed.git.host}
<CloneButton seedHost={seed.git.host} {id} />
{/if}
<span>
{#if seed.addr.host}
<HeaderToggleLabel
+12 -7
View File
@@ -136,15 +136,20 @@
</header>
<main>
<div class="comments">
<Comment comment={issue.comment} {getImage} {wallet} />
{#if !window.HEARTWOOD}
<Comment comment={issue.comment} {getImage} {wallet} />
{/if}
{#each issue.discussion as comment}
<Comment {comment} {getImage} {wallet} />
{#if comment.replies}
<div class="replies">
{#each comment.replies as reply}
<Comment comment={reply} {getImage} {wallet} />
{/each}
</div>
{#if !window.HEARTWOOD}
{#if comment.replies}
<div class="replies">
{#each comment.replies as reply}
<Comment comment={reply} {getImage} {wallet} />
{/each}
</div>
{/if}
{/if}
{/each}
</div>
+3 -1
View File
@@ -138,8 +138,10 @@
</div>
{:else if activeTab === PatchTab.Diff && revision.changeset}
<Changeset
stats={window.HEARTWOOD
? revision.changeset.diff.stats
: revision.changeset.stats}
diff={revision.changeset.diff}
stats={revision.changeset.stats}
on:browse={e => onBrowse(e, revision.oid)} />
{:else if activeTab === PatchTab.Diff}
<Placeholder emoji="🍳">
@@ -66,11 +66,13 @@
{:else if element.type === TimelineType.Thread}
<div class="margin-left">
<Comment comment={element.inner} {wallet} {getImage} />
<div class="replies">
{#each element.inner.replies as comment}
<Comment caption="replied" {comment} {wallet} {getImage} />
{/each}
</div>
{#if !window.HEARTWOOD}
<div class="replies">
{#each element.inner.replies as comment}
<Comment caption="replied" {comment} {wallet} {getImage} />
{/each}
</div>
{/if}
</div>
{/if}
{/each}
+3 -5
View File
@@ -3,8 +3,6 @@
import { createEventDispatcher } from "svelte";
import { ObjectType } from "@app/lib/project";
import File from "./Tree/File.svelte";
import Folder from "./Tree/Folder.svelte";
@@ -20,11 +18,11 @@
</script>
{#each tree.entries as entry (entry.path)}
{#if entry.info.objectType === ObjectType.Tree}
{#if window.HEARTWOOD ? entry.kind === "tree" : entry.info.objectType === "TREE"}
<Folder
{fetchTree}
{loadingPath}
name={entry.info.name}
name={window.HEARTWOOD ? entry.name : entry.info.name}
prefix={`${entry.path}/`}
currentPath={path}
on:select={onSelect} />
@@ -32,7 +30,7 @@
<File
active={entry.path === path}
loading={entry.path === loadingPath}
name={entry.info.name}
name={window.HEARTWOOD ? entry.name : entry.info.name}
on:click={() => onSelect({ detail: entry.path })} />
{/if}
{/each}
+3 -4
View File
@@ -2,7 +2,6 @@
import type { MaybeTree } from "@app/lib/project";
import Loading from "@app/components/Loading.svelte";
import { ObjectType } from "@app/lib/project";
import { createEventDispatcher } from "svelte";
import File from "./File.svelte";
@@ -75,10 +74,10 @@
{:then tree}
{#if tree}
{#each tree.entries as entry (entry.path)}
{#if entry.info.objectType === ObjectType.Tree}
{#if window.HEARTWOOD ? entry.kind === "tree" : entry.info.objectType === "TREE"}
<svelte:self
{fetchTree}
name={entry.info.name}
name={window.HEARTWOOD ? entry.name : entry.info.name}
on:select={onSelectFile}
prefix={`${entry.path}/`}
{loadingPath}
@@ -87,7 +86,7 @@
<File
active={entry.path === currentPath}
loading={entry.path === loadingPath}
name={entry.info.name}
name={window.HEARTWOOD ? entry.name : entry.info.name}
on:click={() => {
onSelectFile({ detail: entry.path });
}} />
+3 -1
View File
@@ -124,7 +124,9 @@
name: "id",
label: "Radicle",
validate: "identity",
placeholder: "Radicle ID, eg. rad:git:hnrkqdpm9ub19oc8d…",
placeholder: window.HEARTWOOD
? "Radicle ID, eg. rad:zKtT7DmF9H34KkvcKj…"
: "Radicle ID, eg. rad:git:hnrkqdpm9ub19oc8d…",
description: "The local radicle identity associated with this name.",
value: r.profile.id ?? "",
editable: true,
+12
View File
@@ -251,6 +251,10 @@ test("clone modal", async ({ page }) => {
});
test("peer and branch switching", async ({ page }) => {
if (process.env.HEARTWOOD) {
test.skip();
}
await page.goto(projectFixtureUrl);
// Alice's peer.
@@ -335,6 +339,10 @@ test("peer and branch switching", async ({ page }) => {
});
test("only one modal can be open at a time", async ({ page }) => {
if (process.env.HEARTWOOD) {
test.skip();
}
await page.goto(projectFixtureUrl);
await page.getByTitle("Change peer").click();
@@ -366,6 +374,10 @@ test("only one modal can be open at a time", async ({ page }) => {
});
test.describe("browser error handling", () => {
if (process.env.HEARTWOOD) {
test.skip();
}
test("error appears when folder can't be loaded", async ({ page }) => {
await page.route(
`**/v1/projects/${ridPrefix}${rid}/tree/${aliceMainHead}/markdown/`,
+20
View File
@@ -8,6 +8,10 @@ import {
const modifiedFileFixture = `${projectFixtureUrl}/remotes/${bobRemote}/commits/2b32f6fe50090ebdb4cd7441e943330da3e6ff04`;
test("navigation from commit list", async ({ page }) => {
if (process.env.HEARTWOOD) {
test.skip();
}
await page.goto(projectFixtureUrl);
await page.getByTitle("Change peer").click();
await page.locator("text=bob hyyzz9").click();
@@ -18,6 +22,10 @@ test("navigation from commit list", async ({ page }) => {
});
test("relative timestamps", async ({ page }) => {
if (process.env.HEARTWOOD) {
test.skip();
}
await page.addInitScript(() => {
window.initializeTestStubs = () => {
window.e2eTestStubs.FakeTimers.install({
@@ -34,6 +42,10 @@ test("relative timestamps", async ({ page }) => {
});
test("modified file", async ({ page }) => {
if (process.env.HEARTWOOD) {
test.skip();
}
await page.goto(modifiedFileFixture);
// Commit header.
@@ -59,6 +71,10 @@ test("modified file", async ({ page }) => {
});
test("created file", async ({ page }) => {
if (process.env.HEARTWOOD) {
test.skip();
}
await page.goto(
`${projectFixtureUrl}/remotes/${bobRemote}/commits/d6318f7f3d9c15b8ac6dd52267c53220d00f0982`,
);
@@ -69,6 +85,10 @@ test("created file", async ({ page }) => {
});
test("deleted file", async ({ page }) => {
if (process.env.HEARTWOOD) {
test.skip();
}
await page.goto(
`${projectFixtureUrl}/remotes/${bobRemote}/commits/cd13c2d9a8a930d64a82b6134b44d1b872e33662`,
);
+12
View File
@@ -6,6 +6,10 @@ import {
} from "@tests/support/fixtures.js";
test("peer and branch switching", async ({ page }) => {
if (process.env.HEARTWOOD) {
test.skip();
}
await page.goto(projectFixtureUrl);
await page.locator('role=button[name="Commit count"]').click();
@@ -83,6 +87,10 @@ test("peer and branch switching", async ({ page }) => {
});
test("verified badge", async ({ page }) => {
if (process.env.HEARTWOOD) {
test.skip();
}
await page.goto(projectFixtureUrl);
await page.locator('role=button[name="Commit count"]').click();
@@ -101,6 +109,10 @@ test("verified badge", async ({ page }) => {
});
test("relative timestamps", async ({ page }) => {
if (process.env.HEARTWOOD) {
test.skip();
}
await page.addInitScript(() => {
window.initializeTestStubs = () => {
window.e2eTestStubs.FakeTimers.install({
Binary file not shown.
+39 -9
View File
@@ -160,7 +160,11 @@ function log(text: string, label: string, outputLog: Stream.Writable) {
}
}
export function appConfigWithFixture() {
export const appConfigWithFixture = process.env.HEARTWOOD
? configFixtureHeartwood
: configFixture;
export function configFixture() {
window.APP_CONFIG = {
walletConnect: {
bridge: "https://radicle.bridge.walletconnect.org",
@@ -181,15 +185,41 @@ export function appConfigWithFixture() {
};
}
export function configFixtureHeartwood() {
window.APP_CONFIG = {
walletConnect: {
bridge: "https://radicle.bridge.walletconnect.org",
},
reactions: [],
seeds: {
pinned: [{ host: "0.0.0.0", emoji: "🚀" }],
},
projects: {
pinned: [
{
name: "source-browsing",
id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
seed: "0.0.0.0",
},
],
},
};
}
export const aliceMainHead = "fcc929424b82984b7cbff9c01d2e20d9b1249842";
export const aliceRemote =
"hybg18bc4cu8z9xtj44skxperfdpxpp1wp8zygyzti5kfiggdizfxy";
export const aliceRemote = process.env.HEARTWOOD
? "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
: "hybg18bc4cu8z9xtj44skxperfdpxpp1wp8zygyzti5kfiggdizfxy";
export const bobRemote =
"hyyzz9w4ffg16zftjki3enajm4mkqkayb5ch1p6ns3f83np1hqkrp6";
export const rid = "hnrkdi8be7n4hhqoz9rpzrgd68u9dr3zsxgmy";
export const ridPrefix = "rad:git:";
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 = 8777;
export const seedVersion = "0.2.0";
export const seedRemote =
"hybuytx44z9cfsm5739wecia9j4b7expgc15qkazph59szp57m4d3o";
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
? "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
: "hybuytx44z9cfsm5739wecia9j4b7expgc15qkazph59szp57m4d3o";
+13 -7
View File
@@ -9,24 +9,30 @@ export default async function globalSetup(_config: FullConfig): Promise<void> {
// Assert that the test http-api is running. If it is not running, throw an
// error that explains how to run it.
async function assertHttpApiRunning(): Promise<void> {
const palmTestFixtureSeedId = seedRemote;
const notRunningMessage =
"The http-api server with test fixtures needs to be running.\n" +
"👉 You can start it with `./scripts/run-http-api-with-fixtures`\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");
let peerId: string | undefined = undefined;
try {
const response = await fetch(`http://0.0.0.0:${seedPort}`);
const data = await response.json();
peerId = data.peer.id;
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;
}
} catch (err) {
console.error(err);
throw new Error(notRunningMessage);
}
if (peerId !== palmTestFixtureSeedId) {
if (peerId !== seedRemote) {
const wrongSeedMessage = `The server on port ${seedPort} doesn't have the right fixtures.\n`;
throw new Error(wrongSeedMessage + notRunningMessage);
}
+4 -5
View File
@@ -101,11 +101,11 @@ describe("routeToPath", () => {
params: {
view: { resource: "tree" },
seed: "willow.radicle.garden",
id: "rad:git:hnrkmg77m8tfzj4gi4pa4mbhgysfgzwntjpao",
id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
},
},
output:
"/seeds/willow.radicle.garden/rad:git:hnrkmg77m8tfzj4gi4pa4mbhgysfgzwntjpao/tree",
"/seeds/willow.radicle.garden/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT/tree",
description: "Seed Project Route",
},
])("$description", (route: any) => {
@@ -219,8 +219,7 @@ describe("pathToRoute", () => {
description: "registrations Submit Route",
},
{
input:
"/seeds/willow.radicle.garden/rad:git:hnrkmg77m8tfzj4gi4pa4mbhgysfgzwntjpao",
input: "/seeds/willow.radicle.garden/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
output: {
resource: "projects",
params: {
@@ -228,7 +227,7 @@ describe("pathToRoute", () => {
seed: "willow.radicle.garden",
profile: undefined,
peer: undefined,
id: "rad:git:hnrkmg77m8tfzj4gi4pa4mbhgysfgzwntjpao",
id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
},
},
description: "Seed Project Route",
+12 -4
View File
@@ -37,9 +37,15 @@ describe("Format functions", () => {
});
test("formatRadicleId", () => {
expect(
utils.formatRadicleId("rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o"),
).toEqual("rad:git:hnrkem…d84c9o");
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", () => {
@@ -117,7 +123,9 @@ describe("String Assertions", () => {
});
test.each([
{ id: "rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o", expected: true },
process.env.HEARTWOOD
? { id: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT", expected: true }
: { id: "rad:git:hnrkemobagsicpf9sr95o3g551otspcd84c9o", expected: true },
{ id: "0x1234567890123456789012345678901234567890", expected: false },
])("isRadicleId $id => $expected", ({ id, expected }) => {
expect(utils.isRadicleId(id)).toEqual(expected);
+10
View File
@@ -5,6 +5,16 @@ test.use({
});
test("landing page", async ({ page }) => {
await page.addInitScript(() => {
window.initializeTestStubs = () => {
window.e2eTestStubs.FakeTimers.install({
now: new Date("November 24 2022 12:00:00").valueOf(),
shouldClearNativeTimers: true,
shouldAdvanceTime: false,
});
};
});
await page.addInitScript(appConfigWithFixture);
await page.goto("/", { waitUntil: "networkidle" });
await expect(page).toHaveScreenshot();
+10
View File
@@ -1,6 +1,16 @@
import { test, expect } from "@tests/support/fixtures.js";
test("seed page", async ({ page }) => {
await page.addInitScript(() => {
window.initializeTestStubs = () => {
window.e2eTestStubs.FakeTimers.install({
now: new Date("November 24 2022 12:00:00").valueOf(),
shouldClearNativeTimers: true,
shouldAdvanceTime: false,
});
};
});
await page.goto("/seeds/radicle.local", { waitUntil: "networkidle" });
await expect(page).toHaveScreenshot();
});
+1
View File
@@ -12,6 +12,7 @@ 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