Files
radicle-explorer/src/project.ts
T
Sebastian Martinez e0ee26f2a1 Refactor api calls
Signed-off-by: Sebastian Martinez <sebastinez@me.com>
2022-05-18 15:45:47 +02:00

384 lines
9.3 KiB
TypeScript

import { navigate } from 'svelte-routing';
import { get, writable } from 'svelte/store';
import { type Host, Request } from '@app/api';
import type { Commit, CommitHeader, CommitsHistory } from '@app/commit';
import { isOid, isRadicleId } from '@app/utils';
import { Profile, ProfileType } from '@app/profile';
import { Seed } from '@app/base/seeds/Seed';
import type { Config } from '@app/config';
export type Urn = string;
export type PeerId = string;
export type Branches = { [key: string]: string };
export interface Anchor {
confirmed: true;
id: string;
anchor: {
stateHash: string;
};
}
export interface PendingAnchor {
confirmed: false;
id: string;
safeTxHash: string; // Safe transaction hash.
confirmations: string[]; // Owner addresses who have confirmed.
anchor: {
stateHash: string;
};
}
// Enumerates the space below the Header component in the projects View component
export enum ProjectContent {
Tree,
History,
Commit,
}
export interface ProjectInfo {
head: string | null;
urn: string;
name: string;
description: string;
defaultBranch: string;
maintainers: Urn[];
delegates: PeerId[];
}
export interface Tree {
path: string;
info: EntryInfo;
entries: Array<Entry>;
stats: Stats;
}
export interface Stats {
commits: number;
contributors: number;
}
export enum ObjectType {
Blob = "BLOB",
Tree = "TREE",
}
export interface EntryInfo {
name: string;
objectType: ObjectType;
lastCommit: CommitHeader;
}
export interface Entry {
path: string;
info: EntryInfo;
}
export interface Blob {
binary?: boolean;
html?: boolean;
content: string;
path: string;
info: EntryInfo;
}
export interface Remote {
heads: Branches;
}
export interface Person {
name: string;
}
export interface Peer {
id: PeerId;
person?: Person;
delegate: boolean;
}
export interface Browser {
content: ProjectContent;
revision: string | null;
peer: string | null;
path: string | null;
line: number | null;
}
export const browserStore = writable({
content: ProjectContent.Tree,
branches: {},
revision: null,
peer: null,
path: null,
line: null,
} as Browser);
export interface BrowseTo {
content?: ProjectContent;
revision?: string | null;
path?: string | null;
peer?: string | null;
line?: number | null;
}
export interface PathOptions extends BrowseTo {
urn: string;
profile?: string | null;
seed?: string | null;
}
export function browse(browse: BrowseTo): void {
const browser = get(browserStore);
browserStore.set({ ...browser, ...browse });
}
export function path(opts: PathOptions): string {
const { urn, profile, seed, peer, content, revision, path } = opts;
const result = [];
if (profile) {
result.push(profile);
} else if (seed) {
result.push("seeds", seed);
}
result.push(urn);
if (peer) {
result.push("remotes", peer);
}
switch (content) {
case ProjectContent.History:
result.push("history");
break;
case ProjectContent.Commit:
result.push("commits");
break;
default:
result.push("tree");
break;
}
if (revision) {
result.push(revision);
}
// Avoids appending a slash when the path is the root directory.
if (path && path !== "/") {
result.push(path);
}
return "/" + result.join("/");
}
// We need a SHA1 commit in some places, so we return early if the revision is a SHA and else we look into branches.
export function getOid(revision: string, branches?: Branches): string | null {
if (isOid(revision)) return revision;
if (branches) {
const oid = branches[revision];
if (oid) {
return oid;
}
}
return null;
}
// Parses the path consisting of a revision (eg. branch or commit) and file path into a tuple [revision, file-path]
export function parseRoute(input: string, branches: Branches): { path?: string; revision?: string } {
const branch = Object.entries(branches).find(([branchName,]) => input.startsWith(branchName));
const commitPath = [input.slice(0, 40), input.slice(41)];
const parsed: { path?: string; revision?: string } = {};
if (branch) {
const [rev, path] = [input.slice(0, branch[0].length), input.slice(branch[0].length + 1)];
parsed.revision = rev;
parsed.path = path ? path : "/";
} else if (isOid(commitPath[0])) {
parsed.revision = commitPath[0];
parsed.path = commitPath[1] ? commitPath[1] : "/";
} else {
parsed.path = input;
}
return parsed;
}
export class Project implements ProjectInfo {
urn: string;
head: string | null;
name: string;
description: string;
defaultBranch: string;
maintainers: Urn[];
delegates: PeerId[];
seed: Seed;
peers: Peer[];
branches: Branches;
profile: Profile | null;
anchors: string[];
constructor(urn: string, info: ProjectInfo, seed: Seed, peers: Peer[], branches: Branches, profile: Profile | null, anchors: string[]) {
this.urn = urn;
this.head = info.head;
this.name = info.name;
this.description = info.description;
this.defaultBranch = info.defaultBranch;
this.maintainers = info.maintainers;
this.delegates = info.delegates;
this.seed = seed;
this.peers = peers;
this.branches = branches;
this.profile = profile;
this.anchors = anchors;
}
async getRoot(
revision: string | null,
): Promise<{ tree: Tree; commit: string }> {
const head = this.branches[this.defaultBranch];
const commit = revision ? getOid(revision, this.branches) : head;
if (! commit) {
throw new Error(`Revision ${revision} not found`);
}
const tree = await this.getTree(commit, "/");
return { tree, commit };
}
static async getInfo(nameOrUrn: string, host: Host): Promise<ProjectInfo> {
const info = await new Request(`projects/${nameOrUrn}`, host).get();
return {
...info,
...info.meta // Nb. This is only needed while we are upgrading to the new http-api.
};
}
static async getProjects(host: Host): Promise<ProjectInfo[]> {
return new Request("projects", host).get();
}
static async getDelegateProjects(delegate: string, host: Host): Promise<ProjectInfo[]> {
return new Request(`delegates/${delegate}/projects`, host).get();
}
static async getRemote(urn: string, peer: string, host: Host): Promise<Remote> {
return new Request(`projects/${urn}/remotes/${peer}`, host).get();
}
static async getRemotes(urn: string, host: Host): Promise<Peer[]> {
return new Request(`projects/${urn}/remotes`, host).get();
}
static async getCommits(
urn: string,
host: Host,
opts?: {
parent?: string | null;
since?: string;
until?: string;
perPage?: number;
page?: number;
verified?: boolean;
}
): Promise<CommitsHistory> {
const params: Record<string, any> = {
"parent": opts?.parent,
"since": opts?.since,
"until": opts?.until,
"per-page": opts?.perPage,
"page": opts?.page,
"verified": opts?.verified
};
return new Request(`projects/${urn}/commits`, host).get(params);
}
async getCommit(commit: string): Promise<Commit> {
return new Request(`projects/${this.urn}/commits/${commit}`, this.seed.api).get();
}
async getTree(
commit: string,
path: string,
): Promise<Tree> {
if (path === "/") path = "";
return new Request(`projects/${this.urn}/tree/${commit}/${path}`, this.seed.api).get();
}
async getBlob(
commit: string,
path: string,
options: { highlight: boolean },
): Promise<Blob> {
return new Request(`projects/${this.urn}/blob/${commit}/${path}`, this.seed.api).get(options);
}
async getReadme(
commit: string,
): Promise<Blob> {
return new Request(`projects/${this.urn}/readme/${commit}`, this.seed.api).get();
}
navigateTo(browse: BrowseTo): void {
navigate(this.pathTo(browse));
}
pathTo(browse: BrowseTo): string {
const browser = get(browserStore);
const options: PathOptions = {
urn: this.urn,
...browser,
...browse
};
if (this.profile) {
options.profile = this.profile?.nameOrAddress;
} else {
options.seed = this.seed.host;
}
return path(options);
}
static async get(id: string, peer: string | null, profileName: string | null, seedHost: string | null, config: Config): Promise<Project> {
const profile = profileName ? await Profile.get(profileName, ProfileType.Project, config) : null;
const seed = profile ? profile.seed : seedHost ? await Seed.lookup(seedHost, config) : null;
if (!profile && !seed) {
throw new Error("Couldn't load project");
}
if (! seed?.valid) {
throw new Error("Couldn't load project: invalid seed");
}
const info = await Project.getInfo(id, seed.api);
const urn = isRadicleId(id) ? id : info.urn;
const anchors = profile ? await profile.confirmedProjectAnchors(urn, config) : [];
// Older versions of http-api don't include the URN.
if (! info.urn) info.urn = urn;
const peers: Peer[] = info.delegates
? await Project.getRemotes(urn, seed.api)
: [];
let remote: Remote = {
heads: info.head ? { [info.defaultBranch]: info.head } : {}
};
if (peer) {
try {
remote = await Project.getRemote(urn, peer, seed.api);
} catch {
remote.heads = {};
}
}
return new Project(urn, info, seed, peers, remote.heads, profile, anchors);
}
}