Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add branch browsing and selector
Sebastian Martinez committed 4 years ago
commit ddf7b27f551021554389861fd2f5991122ac2e44
parent 157e025ecad155bdf501c1b18ead1622d1c8704d
10 files changed +393 -175
added src/base/projects/BranchSelector.svelte
@@ -0,0 +1,139 @@
+
<script lang="ts">
+
  import { Info, getOid } from "@app/project";
+
  import { formatCommit, isOid } from "@app/utils";
+
  import { createEventDispatcher } from "svelte";
+

+
  export let branches: [string, string][];
+
  export let project: Info;
+
  export let revision: string;
+

+
  const dispatch = createEventDispatcher();
+

+
  let branchesDropdown = false;
+
  const switchBranch = (name: string) => {
+
    dispatch("revisionChanged", name);
+
  };
+

+
  // Sort branches array alphabetically
+
  const sortBranches = ([firstBranchName,]: [string, string], [secondBranchName,]: [string, string]) => {
+
    if (firstBranchName < secondBranchName) return -1;
+
    if (firstBranchName > secondBranchName) return 1;
+
    return 0;
+
  };
+

+
  branches = branches.sort(sortBranches);
+

+
  // Casting commit to string, since the commit will always be defined here
+
  $: commit = getOid(project.head, revision, branches);
+
</script>
+

+
<style>
+
  .commit {
+
    display: flex;
+
    align-items: center;
+
    justify-content: center;
+
    font-family: var(--font-family-monospace);
+
  }
+
  .commit .branch {
+
    cursor: pointer;
+
    padding: 0.5rem 0.75rem;
+
    color: var(--color-secondary);
+
    background-color: var(--color-secondary-background);
+
    border-radius: 0.25rem 0 0 0.25rem;
+
  }
+
  .commit .branch.not-allowed {
+
    cursor: not-allowed;
+
  }
+
  .branch:hover {
+
    background-color: var(--color-foreground-background-lighter);
+
  }
+
  .commit .hash {
+
    display: inline-block;
+
    color: var(--color-secondary);
+
    background-color: var(--color-secondary-background-darker);
+
    padding: 0.5rem 0.75rem;
+
    border-radius: inherit;
+
  }
+
  .item {
+
    cursor: pointer;
+
    padding: 0.3rem;
+
  }
+
  .item:hover {
+
    background-color: var(--color-foreground-background-lighter);
+
  }
+
  .dropdown {
+
    background-color: var(--color-foreground-background);
+
    padding: 1rem;
+
    margin-top: 0.5rem;
+
    border-radius: 0.25rem;
+
    display: none;
+
    position: absolute;
+
  }
+
  .branch-dropdown.branch-dropdown-visible {
+
    display: block;
+
  }
+
  .stat {
+
    font-family: var(--font-family-monospace);
+
    padding: 0.5rem 0.75rem;
+
    background: var(--color-foreground-background);
+
  }
+
  @media (max-width: 720px) {
+
    .dropdown {
+
      left: 32px;
+
      z-index: 10;
+
    }
+
  }
+
</style>
+

+
<div class="commit">
+
  <!-- Check for branches listing feature -->
+
  {#if branches.length > 0}
+
    <span>
+
      <div on:click={() => branchesDropdown = !branchesDropdown} class="stat branch" class:not-allowed={!branches}>
+
        {#if commit === project.head}
+
          {project.meta.defaultBranch}
+
        <!-- If commit is no sha1 commit show branch or tag name -->
+
        {:else if !isOid(revision)}
+
          {revision}
+
        {:else}
+
          Browse...
+
        {/if}
+
      </div>
+
      <div
+
        class="dropdown branch-dropdown"
+
        class:branch-dropdown-visible={branchesDropdown}
+
      >
+
        {#each branches as [name,]}
+
          <div class="item" on:click={() => switchBranch(name)}>{name}</div>
+
        {/each}
+
      </div>
+
    </span>
+
    {#if commit === project.head || !isOid(revision)}
+
      <div class="hash">
+
        {formatCommit(commit)}
+
      </div>
+
    {:else}
+
      <div class="hash desktop">
+
        {commit}
+
      </div>
+
      <div class="hash mobile">
+
        {formatCommit(commit)}
+
      </div>
+
    {/if}
+
  <!-- If there is no branch listing available, show default branch name if commit is head and else show entire commit -->
+
  {:else if commit === project.head}
+
    <div class="stat branch not-allowed">
+
      {project.meta.defaultBranch}
+
    </div>
+
    <div class="hash">
+
      {formatCommit(commit)}
+
    </div>
+
  {:else}
+
    <div class="hash desktop">
+
      {commit}
+
    </div>
+
    <div class="hash mobile">
+
      {formatCommit(commit)}
+
    </div>
+
  {/if}
+
</div>
modified src/base/projects/Browser.svelte
@@ -1,4 +1,5 @@
<script lang="ts">
+
  import { createEventDispatcher, onMount } from 'svelte';
  import { navigate } from 'svelte-routing';
  import type { Config } from '@app/config';
  import * as proj from '@app/project';
@@ -9,6 +10,8 @@
  import Blob from './Blob.svelte';
  import Readme from './Readme.svelte';

+
  const dispatch = createEventDispatcher();
+

  enum Status {
    Loading,
    Loaded,
@@ -18,18 +21,24 @@
    | { status: Status.Loaded; path: string; blob: proj.Blob };

  export let urn: string;
-
  export let commit: string;
+
  export let revision: string;
  export let config: Config;
  export let path: string;
  export let org: string | null = null;
  export let user: string | null = null;
  export let tree: proj.Tree;
+
  export let project: proj.Info;
+
  export let branches: [string, string][];

  // When the component is loaded the first time, the blob is yet to be loaded.
  let state: State = { status: Status.Loading, path };
  // Whether the mobile file tree is visible.
  let mobileFileTree = false;

+
  onMount(() => {
+
    dispatch("routeParamsChange", { content: proj.ProjectContent.Tree, revision, path });
+
  });
+

  const loadBlob = async (path: string): Promise<proj.Blob> => {
    if (state.status == Status.Loaded && state.path === path) {
      return state.blob;
@@ -46,28 +55,30 @@
    return state.blob;
  };

-
  const onSelect = async ({ detail: path }: { detail: string }) => {
+
  const onSelect = async ({ detail: newPath }: { detail: string }) => {
    // Ensure we don't spend any time in a "loading" state. This means
    // the loading spinner won't be shown, and instead the blob will be
    // displayed once loaded.
-
    const blob = await loadBlob(path);
+
    const blob = await loadBlob(newPath);
    getBlob = new Promise(resolve => resolve(blob));

+
    dispatch("routeParamsChange", { content: proj.ProjectContent.Tree, revision, path });
+

    // Close mobile tree if user navigates to other file
    mobileFileTree = false;
-
    navigateBrowser(commit, path);
+
    navigateBrowser(revision, newPath);
  };

-
  const navigateBrowser = (commit: string, path?: string) => {
+
  const navigateBrowser = (revision: string, path?: string) => {
    // Replaces path with current path if none passed.
    if (path === undefined) path = state.path;

    if (org) {
-
      navigate(proj.path({ urn, org, commit, path }));
+
      navigate(proj.path({ urn, org, revision, path }));
    } else if (user) {
-
      navigate(proj.path({ urn, user, commit, path }));
+
      navigate(proj.path({ urn, user, revision, path }));
    } else {
-
      navigate(proj.path({ urn, commit, path }));
+
      navigate(proj.path({ urn, revision, path }));
    }
  };

@@ -81,6 +92,9 @@

  // This is reactive to respond to path changes that don't originate from this
  // component, eg. when using the browser's "back" button.
+
  $: [revision, path] = proj.splitPrefixFromPath(revision, branches, project.head);
+
  $: commit = proj.getOid(project.head, revision, branches);
+
  $: dispatch("routeParamsChange", { content: proj.ProjectContent.Tree, revision, path });
  $: getBlob = loadBlob(path);
  $: loadingPath = state.status == Status.Loading ? state.path : null;
</script>
modified src/base/projects/Commit/History.svelte
@@ -1,18 +1,26 @@
<script lang="ts">
+
  import { createEventDispatcher, onMount } from "svelte";
  import CommitTeaser from "./CommitTeaser.svelte";
-
  import { getCommits } from "@app/project";
+
  import { getCommits, Info, getOid, ProjectContent } from "@app/project";
  import type { Config } from "@app/config";
  import Loading from "@app/Loading.svelte";
  import { groupCommitHistory, GroupedCommitsHistory } from "./lib";

-
  export let commit: string;
+
  export let revision: string;
  export let urn: string;
  export let config: Config;
+
  export let project: Info;
+
  export let branches: [string, string][];

  async function fetchCommits(): Promise<GroupedCommitsHistory> {
-
    const commitsQuery = await getCommits(urn, commit, config);
+
    const commitsQuery = await getCommits(urn, getOid(project.head, revision, branches), config);
    return groupCommitHistory(commitsQuery);
  }
+

+
  const dispatch = createEventDispatcher();
+
  onMount(() => {
+
    dispatch("routeParamsChange", { content: ProjectContent.History, revision, path: "/" });
+
  });
</script>

<style>
@@ -31,7 +39,7 @@
  .commit {
    padding: 0.25rem 0;
  }
-
  @media (max-width: 720px) {
+
  @media (max-width: 960px) {
    .history {
      padding-left: 2rem;
    }
modified src/base/projects/Header.svelte
@@ -3,18 +3,24 @@
  import * as utils from '@app/utils';
  import Loading from '@app/Loading.svelte';
  import { ethers } from "ethers";
-
  import type { Info, Tree } from '@app/project';
-
  import { ProjectContent } from "@app/project";
+
  import { ProjectContent, getOid } from '@app/project';
+
  import type { Info, Tree } from "@app/project";
  import type { Profile } from '@app/profile';
+
  import BranchSelector from './BranchSelector.svelte';
+
  import { createEventDispatcher } from 'svelte';
+

+
  const dispatch = createEventDispatcher();

  export let config: Config;
  export let anchors: string | null = null;
  export let urn: string;
-
  export let commit: string;
+
  export let path: string;
  export let project: Info;
  export let profile: Profile | null = null;
  export let tree: Tree;
+
  export let branches: [string, string][] = [];
  export let content: ProjectContent;
+
  export let revision: string;

  // Whether the clone dropdown is visible.
  let cloneDropdown = false;
@@ -22,8 +28,12 @@
  let seedDropdown = false;

  // Switches between the browser and commit view
-
  const switchContent = () => {
-
    content = content == ProjectContent.Browser ? ProjectContent.Commits : ProjectContent.Browser;
+
  const toggleContent = (input: ProjectContent) => {
+
    dispatch("routeParamsChange", { content: content === input ? ProjectContent.Tree : input, revision, path });
+
  };
+

+
  const updateRevision = (newRevision: string) => {
+
    dispatch("routeParamsChange", { content, revision: newRevision, path });
  };

  const GetAllAnchors = `
@@ -47,12 +57,12 @@
    const unpadded = utils.decodeRadicleId(urn);
    const id = ethers.utils.hexZeroPad(unpadded, 32);
    const allAnchors = await utils.querySubgraph(config.orgs.subgraph, GetAllAnchors, { project: id, org: anchors });
-
    console.log(allAnchors);
    return allAnchors.anchors
      .map((anchor: AnchorObject) => utils.formatProjectHash(ethers.utils.arrayify(anchor.multihash)));
  }

  $: getAnchor = anchors ? getAllAnchors(anchors, urn) : null;
+
  $: commit = getOid(project.head, revision, branches);
</script>

<style>
@@ -65,37 +75,11 @@
    justify-content: left;
    flex-wrap: wrap;
    gap: 0.5rem;
-

  }
  header > * {
    border-radius: 0.25rem;
    min-width: max-content;
  }
-

-
  .commit {
-
    display: flex;
-
    align-items: center;
-
    justify-content: center;
-
    font-family: var(--font-family-monospace);
-
  }
-
  .commit .branch {
-
    padding: 0.5rem 0.75rem;
-
    color: var(--color-secondary);
-
    background-color: var(--color-secondary-background);
-
    border-radius: 0.25rem 0 0 0.25rem;
-
  }
-
  .commit .hash {
-
    display: inline-block;
-
    color: var(--color-secondary);
-
    background-color: var(--color-secondary-background);
-
    padding: 0.5rem 0.75rem;
-
    border-radius: inherit;
-
  }
-
  .branch + .hash {
-
    background-color: var(--color-secondary-background-darker);
-
    border-radius: 0 0.25rem 0.25rem 0;
-
  }
-

  .anchor {
    display: inline-flex;
  }
@@ -222,63 +206,50 @@
</style>

<header>
-
  <div class="commit">
-
    {#if commit === project.head}
-
      <div class="branch">
-
        {project.meta.defaultBranch}
-
      </div>
-
      <div class="hash">
-
        {utils.formatCommit(commit)}
-
      </div>
-
    {:else}
-
      <div class="hash desktop">
-
        {commit}
-
      </div>
-
      <div class="hash mobile">
-
        {utils.formatCommit(commit)}
-
      </div>
-
    {/if}
-
  </div>
-
  <div class="anchor">
-
    {#if anchors}
-
      {#await getAnchor}
-
        <Loading small margins />
-
      {:then anchor}
-
        {#if anchor}
-
          <!-- commit is head and latest anchor  -->
-
          {#if commit == anchor[0] && commit === project.head}
-
            <span class="anchor-widget anchor-latest">
-
              <span class="anchor-label" title="{anchors}">latest 🔐</span>
-
            </span>
-
          <!-- commit is not head but latest anchor  -->
-
          {:else if commit == anchor[0] && commit !== project.head}
-
            <span class="anchor-widget" on:click={() => commit = project.head}>
-
              <span class="anchor-label" title="{anchors}">latest 🔐</span>
-
            </span>
-
          <!-- commit is not head a stale anchor  -->
-
          {:else if anchor?.includes(commit)}
-
            <span class="anchor-widget" on:click={() => commit = anchor[0]}>
-
              <span class="anchor-label" title="{anchors}">stale 🔒</span>
-
            </span>
-
          <!-- commit is not anchored, could be head or any other commit  -->
+
  {#if revision}
+
    <BranchSelector {branches} {project} {revision}
+
      on:revisionChanged={(event) => updateRevision(event.detail)} />
+
    <div class="anchor">
+
      {#if anchors}
+
        {#await getAnchor}
+
          <Loading small margins />
+
        {:then anchor}
+
          {#if anchor}
+
            <!-- commit is head and latest anchor  -->
+
            {#if commit == anchor[0] && commit === project.head}
+
              <span class="anchor-widget anchor-latest">
+
                <span class="anchor-label" title="{anchors}">latest 🔐</span>
+
              </span>
+
            <!-- commit is not head but latest anchor  -->
+
            {:else if commit == anchor[0] && commit !== project.head}
+
              <span class="anchor-widget" on:click={() => updateRevision(project.head)}>
+
                <span class="anchor-label" title="{anchors}">latest 🔐</span>
+
              </span>
+
            <!-- commit is not head a stale anchor  -->
+
            {:else if anchor?.includes(commit)}
+
              <span class="anchor-widget" on:click={() => updateRevision(anchor[0])}>
+
                <span class="anchor-label" title="{anchors}">stale 🔒</span>
+
              </span>
+
            <!-- commit is not anchored, could be head or any other commit  -->
+
            {:else}
+
              <span class="anchor-widget not-anchored" on:click={() => updateRevision(anchor[0])}>
+
                <span class="anchor-label">not anchored 🔓</span>
+
              </span>
+
            {/if}
          {:else}
-
            <span class="anchor-widget not-anchored" on:click={() => commit = anchor[0]}>
+
            <!-- commit is not head and neither an anchor, and there are no anchors available  -->
+
            <span class="anchor-widget not-anchored not-allowed">
              <span class="anchor-label">not anchored 🔓</span>
            </span>
          {/if}
-
        {:else}
-
          <!-- commit is not head and neither an anchor, and there are no anchors available  -->
-
          <span class="anchor-widget not-anchored not-allowed">
-
            <span class="anchor-label">not anchored 🔓</span>
+
        {:catch}
+
          <span class="anchor-widget error" title="Not able to fetch anchor from subgraph">
+
            <span class="anchor-label">❌</span>
          </span>
-
        {/if}
-
      {:catch}
-
        <span class="anchor-widget error" title="Not able to fetch anchor from subgraph">
-
          <span class="anchor-label">❌</span>
-
        </span>
-
      {/await}
-
    {/if}
-
  </div>
+
        {/await}
+
      {/if}
+
    </div>
+
  {/if}
  {#if config.seed.git.host}
    <span>
      <div class="clone" on:click={() => (cloneDropdown = !cloneDropdown)}>
@@ -330,7 +301,7 @@
      {/if}
    </div>
  </span>
-
  <div class="stat commit-count" class:active={content == ProjectContent.Commits} on:click={switchContent}>
+
  <div class="stat commit-count" class:active={content == ProjectContent.History} on:click={() => toggleContent(ProjectContent.History)}>
    <strong>{tree.stats.commits}</strong> commit(s)
  </div>
  <div class="stat">
added src/base/projects/ProjectContentRoutes.svelte
@@ -0,0 +1,52 @@
+
<script lang="ts">
+
  import { createEventDispatcher } from "svelte";
+
  import type { Config } from "@app/config";
+
  import type { Info, Tree } from "@app/project";
+
  import { Route, Router } from "svelte-routing";
+
  import Browser from "./Browser.svelte";
+
  import History from "./Commit/History.svelte";
+

+
  export let urn: string;
+
  export let project: Info;
+
  export let config: Config;
+
  export let org: string;
+
  export let tree: Tree;
+
  export let path: string;
+
  export let user: string;
+
  export let branches: [string, string][];
+

+
  const dispatch = createEventDispatcher();
+

+
  function forwardRouteParams({ detail: newParams }: { detail: any }) {
+
    dispatch("routeParamsChange", newParams);
+
  }
+
</script>
+

+
<Router>
+
  <!-- The default action is to render Browser with the default branch head -->
+
  <Route path="/">
+
    <Browser {urn} {org} {user} {config} {tree} {project} {branches}
+
      path={"/"}
+
      revision={project.head}
+
      on:routeParamsChange={forwardRouteParams} />
+
  </Route>
+
  <Route path="/tree">
+
    <Browser {urn} {org} {user} {config} {tree} {project} {branches}
+
      path={"/"}
+
      revision={project.head}
+
      on:routeParamsChange={forwardRouteParams} />
+
  </Route>
+
  <Route path="/tree/*" let:params>
+
    <Browser {urn} {org} {user} {config} {tree} {project} {branches} {path}
+
      revision={params["*"]}
+
      on:routeParamsChange={forwardRouteParams} />
+
  </Route>
+
  <Route path="/history">
+
    <History {urn} revision={project.head} {config} {project} {branches}
+
      on:routeParamsChange={forwardRouteParams} />
+
  </Route>
+
  <Route path="/history/*" let:params>
+
    <History {urn} revision={params["*"]} {config} {project} {branches}
+
      on:routeParamsChange={forwardRouteParams} />
+
  </Route>
+
</Router>
modified src/base/projects/Routes.svelte
@@ -4,44 +4,21 @@
  import type { Config } from '@app/config';

  export let config: Config;
-
</script>
-

-
<Route path="/projects/:urn/head/*" let:params>
-
  <View {config} urn={params.urn} path={params['*'] || "/"} />
-
</Route>

-
<Route path="/projects/:urn/:commit/*" let:params>
-
  <View {config} urn={params.urn} commit={params.commit} path={params['*'] || "/"} />
-
</Route>
+
</script>

-
<Route path="/projects/:urn" let:params>
-
  <View {config} urn={params.urn} path="/" />
+
<Route path="/projects/:urn/*" let:params>
+
  <View {config} urn={params.urn} />
</Route>

<!-- With an Org context -->

-
<Route path="/orgs/:org/projects/:urn/head/*" let:params>
-
  <View {config} org={params.org} urn={params.urn} path={params['*'] || "/"} />
-
</Route>
-

-
<Route path="/orgs/:org/projects/:urn/:commit/*" let:params>
-
  <View {config} org={params.org} urn={params.urn} commit={params.commit} path={params["*"] || "/"} />
-
</Route>
-

-
<Route path="/orgs/:org/projects/:urn" let:params>
-
  <View {config} org={params.org} urn={params.urn} path="/" />
+
<Route path="/orgs/:org/projects/:urn/*" let:params>
+
  <View {config} org={params.org} urn={params.urn} />
</Route>

<!-- With a User context -->

-
<Route path="/users/:user/projects/:urn/head/*" let:params>
-
  <View {config} user={params.user} urn={params.urn} path={params['*'] || "/"} />
-
</Route>
-

-
<Route path="/users/:user/projects/:urn/:commit/*" let:params>
-
  <View {config} user={params.user} urn={params.urn} commit={params.commit} path={params["*"] || "/"} />
-
</Route>
-

-
<Route path="/users/:user/projects/:urn" let:params>
-
  <View {config} user={params.user} urn={params.urn} path="/" />
+
<Route path="/users/:user/projects/:urn/*" let:params>
+
  <View {config} user={params.user} urn={params.urn} />
</Route>
modified src/base/projects/View.svelte
@@ -8,22 +8,22 @@
  import { Profile, ProfileType } from '@app/profile';
  import type { Info } from '@app/project';
  import { formatOrg } from '@app/utils';
+
  import { getOid } from '@app/project';

-
  import Browser from './Browser.svelte';
-
  import Header from './Header.svelte';
-
  import History from "./Commit/History.svelte";
+
  import Header from '@app/base/projects/Header.svelte';
+
  import ProjectContentRoutes from '@app/base/projects/ProjectContentRoutes.svelte';

  export let urn: string;
  export let org = "";
  export let user = "";
-
  export let commit = "";
  export let config: Config;
-
  export let path: string;

-
  let content = proj.ProjectContent.Browser;
  let parentName = formatOrg(org || user, config);
  let pageTitle = parentName ? `${parentName}/${urn}` : urn;
  let projectInfo: Info | null = null;
+
  let revision: string;
+
  let content: proj.ProjectContent;
+
  let path: string;
  let getProject = new Promise<Profile | null>(resolve => {
    if (org) {
      Profile.get(org, ProfileType.Project, config).then(p => resolve(p));
@@ -36,11 +36,14 @@
    const seed = profile?.seed;
    const cfg = seed ? config.withSeed(seed) : config;
    const info = await proj.getInfo(urn, cfg);
-
    commit = commit ? commit : info.head;
-

    projectInfo = info;

-
    return { project: info, config: cfg, profile };
+
    // Checks for delegates returned from seed node, as feature check of the seed node
+
    if (info.meta.delegates) {
+
      const branches = await proj.getBranchesByPeer(urn, info.meta.delegates[0], cfg);
+
      return { project: info, branches: [...Object.entries(branches.heads)], peer: info.meta.delegates[0], config: cfg, profile };
+
    }
+
    return { project: info, branches: Array([info.meta.defaultBranch, info.head]) as [string, string][], config: cfg, profile };
  });

  const parentUrl = (profile: Profile) => {
@@ -61,11 +64,20 @@
    }
  }

+
  function updateRouteParams({ detail: newParams }: { detail: { path: string; revision: string; content: proj.ProjectContent } }) {
+
    let newLocation = proj.path({ urn, user, org, content: newParams.content, revision: newParams.revision, path: newParams.path });
+
    if (newLocation !== window.location.pathname) {
+
      navigate(newLocation);
+
    }
+
    if (content !== newParams.content) content = newParams.content;
+
    if (revision !== newParams.revision) revision = newParams.revision;
+
    if (path !== newParams.path) path = newParams.path;
+
  }
+

  const back = () => window.history.back();
  // React to changes to the project commit. We have to manually
  // set the URL as well, to match the current commit.
-
  $: projectRoot = proj.path({ urn, user, org, commit });
-
  $: navigate(proj.path({ urn, user, org, commit, path }));
+
  $: projectRoot = proj.path({ urn, user, org });
</script>

<style>
@@ -149,23 +161,19 @@
      <div class="urn">{urn}</div>
      <div class="description">{result.project.meta.description}</div>
    </header>
-
    {#await proj.getTree(urn, commit, "/", config)}
-
      <!-- Loading -->
-
    {:then tree}
-
      <Header {urn} {tree}
+
    {#await proj.getTree(urn, getOid(result.project.head, revision, result.branches), "/", config) then tree}
+
      <Header {urn} {tree} {revision} {content} {path}
        anchors={result.profile?.anchorsAccount ?? org}
        config={result.config}
        project={result.project}
-
        bind:commit={commit}
-
        bind:content={content}
-
      />
-
      {#if content == proj.ProjectContent.Browser}
-
        <Browser {urn} {org} {user} {path} {tree}
-
          commit={commit}
-
          config={result.config} />
-
      {:else if content == proj.ProjectContent.Commits}
-
        <History {urn} config={result.config} bind:commit={commit}  />
-
      {/if}
+
        branches={result.branches}
+
        profile={result.profile}
+
        on:routeParamsChange={updateRouteParams} />
+
      <ProjectContentRoutes {urn} {org} {user} {tree} {path}
+
        project={result.project}
+
        branches={result.branches}
+
        config={result.config}
+
        on:routeParamsChange={updateRouteParams} />
    {:catch err}
      <div class="container center-content">
        <div class="error error-message text-xsmall">
modified src/base/projects/Widget.svelte
@@ -41,7 +41,7 @@
          urn: project.id,
          org,
          user,
-
          commit: project.anchor?.stateHash,
+
          revision: project.anchor?.stateHash,
        })
      );
    }
modified src/project.ts
@@ -1,8 +1,11 @@
import type { Config } from '@app/config';
import * as api from '@app/api';
import type { CommitsHistory } from '@app/base/projects/Commit/lib';
+
import { isOid } from '@app/utils';

export type Urn = string;
+
export type Peer = string;
+
export type Branch = { [key: string]: string };

export interface Project {
  id: string;
@@ -18,8 +21,8 @@ export interface PendingProject extends Project {

// Enumerates the space below the Header component in the projects View component
export enum ProjectContent {
-
  Browser,
-
  Commits,
+
  Tree,
+
  History,
}

export interface Info {
@@ -32,6 +35,7 @@ export interface Meta {
  description: string;
  defaultBranch: string;
  maintainers: Urn[];
+
  delegates: Peer[];
}

export interface Tree {
@@ -85,6 +89,10 @@ export interface Blob {
  info: EntryInfo;
}

+
export interface Branches {
+
  heads: Branch;
+
}
+

export async function getInfo(urn: string, config: Config): Promise<Info> {
  return api.get(`projects/${urn}`, {}, config);
}
@@ -97,15 +105,17 @@ export async function getProjects(config: Config): Promise<any> {
  return api.get("projects", {}, config);
}

+
export async function getBranchesByPeer(urn: string, peer: string, config: Config): Promise<Branches> {
+
  return api.get(`projects/${urn}/remotes/${peer}`, {}, config);
+
}
+

export async function getTree(
  urn: string,
  commit: string,
  path: string,
  config: Config
): Promise<Tree> {
-
  if (path === "/") {
-
    path = "";
-
  }
+
  if (path === "/") path = "";
  return api.get(`projects/${urn}/tree/${commit}/${path}`, {}, config);
}

@@ -128,9 +138,9 @@ export async function getReadme(
}

export function path(
-
  opts: { urn: string; org?: string; user?: string; commit?: string; path?: string }
+
  opts: { urn: string; org?: string; content?: ProjectContent; user?: string; revision?: string; path?: string }
): string {
-
  const { urn, org, user, commit, path } = opts;
+
  const { urn, org, user, content, revision, path } = opts;
  const result = [];

  if (org) {
@@ -140,8 +150,18 @@ export function path(
  }
  result.push("projects", urn);

-
  if (commit) {
-
    result.push(commit);
+
  switch (content) {
+
    case ProjectContent.History:
+
      result.push("history");
+
      break;
+

+
    default:
+
      result.push("tree");
+
      break;
+
  }
+

+
  if (revision) {
+
    result.push(revision);
  } else if (path) {
    result.push("head");
  }
@@ -152,3 +172,27 @@ export function 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.
+
// As fallback we use the head commit.
+
export function getOid(head: string, revision: string, branches?: [string, string][]): string {
+
  if (isOid(revision)) return revision;
+
  if (branches) {
+
    const branch = branches.find(([name,]) => name === revision);
+
    return branch ? branch[1] : head;
+
  }
+
  return head;
+
}
+

+
// Splits the path consisting of a revision (eg. branch or commit) and file path into a tuple [revision, file-path]
+
export function splitPrefixFromPath(input: string, branches: [string, string][], head: string): [string, string] {
+
  const branch = branches.find(([branchName,]) => input.startsWith(branchName));
+
  const commitPath = [input.slice(0, 40), input.slice(41)];
+
  if (branch) {
+
    const [rev, path] = [input.slice(0, branch[0].length), input.slice(branch[0].length + 1)];
+
    return [rev, path ? path : "/"];
+
  } else if (isOid(commitPath[0])) {
+
    return [commitPath[0], commitPath[1] ? commitPath[1] : "/"];
+
  }
+
  return [head, "/"];
+
}
modified src/utils.ts
@@ -28,10 +28,10 @@ export interface Safe {
}

export interface SafeTransaction {
-
    to: string;
-
    value: string;
-
    data: string;
-
    operation: number;
+
  to: string;
+
  value: string;
+
  data: string;
+
  operation: number;
}

export interface Token {
@@ -43,17 +43,17 @@ export interface Token {
}

export enum Status {
-
    Signing,
-
    Pending,
-
    Success,
-
    Failed,
-
  }
+
  Signing,
+
  Pending,
+
  Success,
+
  Failed,
+
}

export type State =
-
      { status: Status.Signing }
-
    | { status: Status.Pending }
-
    | { status: Status.Success }
-
    | { status: Status.Failed; error: string };
+
    { status: Status.Signing }
+
  | { status: Status.Pending }
+
  | { status: Status.Success }
+
  | { status: Status.Failed; error: string };

export async function isReverseRecordSet(address: string, domain: string, config: Config): Promise<boolean> {
  const name = await config.provider.lookupAddress(address);
@@ -175,6 +175,11 @@ export function isRadicleId(input: string): boolean {
  return /^rad:[a-z]+:[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);
+
}
+

// Check whether the input is a URL.
export function isUrl(input: string): boolean {
  return /^https?:\/\//.test(input);