change the world: track projects not repos

This commit swaps usage over to the new implementation of `cli/load`
(the one that wraps `api/load`) and makes changes throughout the project
to accomodate that we now track instances by Project rather than by
RepoId.

Test plan: Unit tests updated; run `yarn test --full`. Also, for safety:
actually load a project (by whole org, why not) and verify that the
frontend still works.
This commit is contained in:
Dandelion Mané 2019-07-16 19:31:11 +01:00
parent e31269283a
commit c15e97b4d4
31 changed files with 176 additions and 233 deletions

View File

@ -134,9 +134,7 @@ function getGitState() /*: GitState */ {
const SOURCECRED_GIT_STATE = stringify(getGitState());
process.env.SOURCECRED_GIT_STATE = SOURCECRED_GIT_STATE;
function getClientEnvironment(
repoRegistryContents /*: RepoIdRegistry | null */
) {
function getClientEnvironment(projectIds /*: $ReadOnlyArray<string> | null*/) {
const raw = {};
// Useful for determining whether were running in production mode.
// Most importantly, it switches React into the correct mode.
@ -144,7 +142,7 @@ function getClientEnvironment(
// Used by `src/core/version.js`.
raw.SOURCECRED_GIT_STATE = SOURCECRED_GIT_STATE;
// Optional. Used by `src/homepage/routeData.js`
raw.REPO_REGISTRY = stringify(repoRegistryContents);
raw.PROJECT_IDS = stringify(projectIds);
// Stringify all values so we can feed into Webpack's DefinePlugin.
const stringified = {"process.env": {}};

View File

@ -7,7 +7,6 @@ import type {
} from "express";
import type {RepoIdRegistry} from "../src/core/repoIdRegistry";
*/
const fs = require("fs");
const os = require("os");
const path = require("path");
const webpack = require("webpack");
@ -18,45 +17,22 @@ const StaticSiteGeneratorPlugin = require("static-site-generator-webpack-plugin"
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
const paths = require("./paths");
const getClientEnvironment = require("./env");
const _getProjectIds = require("../src/core/_getProjectIds");
// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
function loadRepoRegistry() /*: RepoIdRegistry */ {
function loadProjectIds() /*: Promise<$ReadOnlyArray<string>> */ {
const env = process.env.SOURCECRED_DIRECTORY;
// TODO(#945): de-duplicate finding the directory with src/cli/common.js
const defaultDirectory = path.join(os.tmpdir(), "sourcecred");
const scDirectory = env != null ? env : defaultDirectory;
// TODO(@dandelion): Remove hacks around compat usage here
// TODO(@dandelion): Import rather than hardcode the registry file name
const registryFile = path.join(scDirectory, "repositoryRegistry.json");
let jsonString;
try {
jsonString = fs.readFileSync(registryFile).toString();
} catch (e) {
if (e.code === "ENOENT") {
jsonString = JSON.stringify([
{version: "0.2.0", type: "REPO_ID_REGISTRY"},
[],
]);
} else {
throw e;
}
}
const json = JSON.parse(jsonString);
const compat = json[0];
if (compat.version !== "0.2.0" || compat.type !== "REPO_ID_REGISTRY") {
throw new Error("Compat mismatch");
}
return json[1];
return _getProjectIds(scDirectory);
}
const repoRegistry = loadRepoRegistry();
// Get environment variables to inject into our app.
const env = getClientEnvironment(repoRegistry);
function makeConfig(mode /*: "production" | "development" */) /*: mixed */ {
async function makeConfig(
mode /*: "production" | "development" */
) /*: Promise<mixed> */ {
return {
// Don't attempt to continue if there are any errors.
bail: true,
@ -211,7 +187,7 @@ function makeConfig(mode /*: "production" | "development" */) /*: mixed */ {
},
],
},
plugins: plugins(mode),
plugins: await plugins(mode),
// Some libraries import Node modules but don't use them in the browser.
// Tell Webpack to provide empty mocks for them so importing them works.
node: {
@ -225,12 +201,14 @@ function makeConfig(mode /*: "production" | "development" */) /*: mixed */ {
};
}
function plugins(mode /*: "development" | "production" */) {
async function plugins(mode /*: "development" | "production" */) {
const projectIds = await loadProjectIds();
const env = getClientEnvironment(projectIds);
const basePlugins = [
new StaticSiteGeneratorPlugin({
entry: "ssr",
paths: require("../src/homepage/routeData")
.makeRouteData(repoRegistry)
.makeRouteData(projectIds)
.map(({path}) => path),
locals: {},
}),

View File

@ -3,8 +3,8 @@ set -eu
usage() {
printf 'usage: build_static_site.sh --target TARGET\n'
printf ' [--project PROJECT [...]]\n'
printf ' [--weights WEIGHTS_FILE]\n'
printf ' [--repo OWNER/NAME [...]]\n'
printf ' [--cname DOMAIN]\n'
printf ' [--no-backend]\n'
printf ' [-h|--help]\n'
@ -13,12 +13,11 @@ usage() {
printf '\n'
printf '%s\n' '--target TARGET'
printf '\t%s\n' 'an empty directory into which to build the site'
printf '%s\n' '--project PROJECT'
printf '\t%s\n' 'a project spec; see help for cli/load.js for details'
printf '%s\n' '--weights WEIGHTS_FILE'
printf '\t%s\n' 'path to a json file which contains a weights configuration.'
printf '\t%s\n' 'This will be used instead of the default weights and persisted.'
printf '%s\n' '--repo OWNER/NAME'
printf '\t%s\n' 'a GitHub repository (e.g., torvalds/linux) for which'
printf '\t%s\n' 'to include example data'
printf '%s\n' '--cname DOMAIN'
printf '\t%s\n' 'configure DNS for a GitHub Pages site to point to'
printf '\t%s\n' 'the provided custom domain'
@ -55,6 +54,7 @@ parse_args() {
cname=
weights=
repos=( )
projects=( )
while [ $# -gt 0 ]; do
case "$1" in
--target)
@ -73,10 +73,10 @@ parse_args() {
if [ $# -eq 0 ]; then die 'missing value for --weights'; fi
weights="$1"
;;
--repo)
--project)
shift
if [ $# -eq 0 ]; then die 'missing value for --repo'; fi
repos+=( "$1" )
if [ $# -eq 0 ]; then die 'missing value for --project'; fi
projects+=( "$1" )
;;
--cname)
shift
@ -140,15 +140,14 @@ build() {
yarn -s backend --output-path "${SOURCECRED_BIN}"
fi
if [ "${#repos[@]}" -ne 0 ]; then
if [ "${#projects[@]}" -ne 0 ]; then
local weightsStr=""
if [ -n "${weights}" ]; then
weightsStr="--weights ${weights}"
fi
for repo in "${repos[@]}"; do
printf >&2 'info: loading repository: %s\n' "${repo}"
for project in "${projects[@]}"; do
NODE_PATH="./node_modules${NODE_PATH:+:${NODE_PATH}}" \
node "${SOURCECRED_BIN:-./bin}/sourcecred.js" load "${repo}" $weightsStr
node "${SOURCECRED_BIN:-./bin}/sourcecred.js" load "${project}" $weightsStr
done
fi

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
[{"type":"sourcecred/project","version":"0.1.0"},{"id":"sourcecred/example-github","repoIds":[{"name":"example-github","owner":"sourcecred"}]}]

View File

@ -1 +0,0 @@
[{"type":"REPO_ID_REGISTRY","version":"0.2.0"},[{"repoId":{"name":"example-github","owner":"sourcecred"}}]]

View File

@ -72,9 +72,9 @@ test_expect_success "should fail with a nonempty directory as target" '
mkdir putative_output
test_expect_success "should fail with missing repo value" '
test_must_fail run --target putative_output --repo 2>err &&
grep -qF -- "missing value for --repo" err &&
test_expect_success "should fail with missing project value" '
test_must_fail run --target putative_output --project 2>err &&
grep -qF -- "missing value for --project" err &&
printf "redacted\n" | test_cmp - important_dir/.wallet.dat
'
@ -98,8 +98,8 @@ test_expect_success "should fail with multiple cname values" '
'
#
# Now, actually generate output in two cases: one with repositories, and
# one with no repositories. We can only do this if we have a token.
# Now, actually generate output in two cases: one with projects, and
# one with no projects. We can only do this if we have a token.
if [ -n "${SOURCECRED_GITHUB_TOKEN:-}" ]; then
test_set_prereq HAVE_GITHUB_TOKEN
@ -114,7 +114,6 @@ run_build() {
description="$1"; shift
output_dir="build_output/output_${prereq_name}"
api_dir="${output_dir}/api/v1/data"
data_dir="${api_dir}/data"
unsafe_arg=
for arg in "${output_dir}" "$@"; do
unusual_chars="$(printf '%s' "$arg" | sed -e 's#[A-Za-z0-9:/_.-]##g')"
@ -133,7 +132,7 @@ run_build() {
run '"${flags}"' >out 2>err &&
test_must_fail grep -vF \
-e "Removing contents of build directory: " \
-e "info: loading repository" \
-e "info: loading project" \
-e "DeprecationWarning: Tapable.plugin is deprecated." \
err &&
test_path_is_dir "${output_dir}" &&
@ -193,63 +192,50 @@ test_pages() {
'
}
run_build TWO_REPOS \
"should build the site with two repositories and a CNAME" \
run_build TWO_PROJECTS \
"should build the site with two projects and a CNAME" \
--no-backend \
--cname sourcecred.example.com \
--repo sourcecred/example-git \
--repo sourcecred/example-github \
--project sourcecred/example-git \
--project sourcecred/example-github \
;
test_pages TWO_REPOS
test_pages TWO_PROJECTS
test_expect_success TWO_REPOS \
"TWO_REPOS: should have a registry with two repositories" '
registry_file="${api_dir}/repositoryRegistry.json" &&
test_path_is_file "${registry_file}" &&
grep -oF "\"name\":" "${registry_file}" | wc -l >actual_count &&
printf "2\n" | test_cmp - actual_count
test_expect_success TWO_PROJECTS \
"TWO_PROJECTS: should have project ids loaded into env" '
grep -F "PROJECT_IDS" out &&
grep -xF "PROJECT_IDS: [\"sourcecred/example-git\",\"sourcecred/example-github\"]" out
'
test_expect_success TWO_REPOS \
"TWO_REPOS: should have a repo registry loaded into env" '
grep -F "REPO_REGISTRY" out &&
grep -xF "REPO_REGISTRY: [{\"repoId\":{\"name\":\"example-git\",\"owner\":\"sourcecred\"}},{\"repoId\":{\"name\":\"example-github\",\"owner\":\"sourcecred\"}}]" out
'
test_expect_success TWO_REPOS \
"TWO_REPOS: should have data for the two repositories" '
for repo in sourcecred/example-git sourcecred/example-github; do
for file in github/view.json.gz; do
test -s "${data_dir}/${repo}/${file}" || return
done
test_expect_success TWO_PROJECTS \
"TWO_PROJECTS: should have data for the two projects" '
# encoded ids for sourcecred/example-git and sourcecred/example-github
for id in c291cmNlY3JlZC9leGFtcGxlLWdpdA c291cmNlY3JlZC9leGFtcGxlLWdpdGh1Yg; do
test -s "${api_dir}/projects/${id}/cred.json" &&
test -s "${api_dir}/projects/${id}/graph.json" ||
return
done
'
test_expect_success TWO_REPOS "TWO_REPOS: should have a correct CNAME record" '
test_expect_success TWO_PROJECTS "TWO_PROJECTS: should have a correct CNAME record" '
test_path_is_file "${output_dir}/CNAME" &&
printf "sourcecred.example.com" | test_cmp - "${output_dir}/CNAME"
'
test_pages NO_REPOS
test_pages NO_PROJECTS
test_expect_success NO_REPOS \
"NO_REPOS: should not have a repository registry" '
registry_file="${api_dir}/repositoryRegistry.json" &&
test_must_fail test -e "${registry_file}"
'
test_expect_success NO_REPOS \
"NO_REPOS: should have empty repo registry loaded into env" '
grep -F "REPO_REGISTRY" out &&
grep -xF "REPO_REGISTRY: []" out
test_expect_success NO_PROJECTS \
"NO_REPOS: should have empty list of project ids loaded into env" '
grep -F "PROJECT_IDS" out &&
grep -xF "PROJECT_IDS: []" out
'
test_expect_success NO_REPOS \
"NO_REPOS: should not have repository data" '
for repo in sourcecred/example-git sourcecred/example-github; do
for file in git/graph.json github/view.json.gz; do
test_must_fail test -f "${data_dir}/${repo}/${file}" || return
for id in c291cmNlY3JlZC9leGFtcGxlLWdpdA== c291cmNlY3JlZC9leGFtcGxlLWdpdGh1Yg==; do
for file in graph.json cred.json; do
test_must_fail test -f "${api_dir}/projects/${id}/${file}" || return
done
done
'

View File

@ -54,28 +54,28 @@ run_without_validation() (
test_expect_success SETUP "should print help message when called without args" '
test_must_fail run_without_validation scores &&
grep -q "no repository ID provided" err &&
grep -q "no project ID provided" err &&
grep -q "sourcecred help scores" err
'
test_expect_success SETUP "help should print usage info" '
run help scores &&
grep -q "usage: sourcecred scores REPO_ID" out
grep -q "usage: sourcecred scores PROJECT_ID" out
'
test_expect_success SETUP "--help should print usage info" '
run scores --help &&
grep -q "usage: sourcecred scores REPO_ID" out
grep -q "usage: sourcecred scores PROJECT_ID" out
'
test_expect_success SETUP "should fail for multiple repos" '
test_expect_success SETUP "should fail for multiple projects" '
test_must_fail run_without_validation scores sourcecred/sourcecred torvalds/linux &&
grep -q "fatal: multiple repository IDs provided" err
grep -q "fatal: multiple project IDs provided" err
'
test_expect_success SETUP "should fail for unloaded repo" '
test_expect_success SETUP "should fail for unloaded project" '
test_must_fail run_without_validation scores torvalds/linux &&
grep -q "fatal: repository ID torvalds/linux not loaded" err
grep -q "fatal: project torvalds/linux not loaded" err
'
if [ -n "${UPDATE_SNAPSHOT}" ]; then

View File

@ -4,7 +4,7 @@
import type {Command} from "./command";
import dedent from "../util/dedent";
import {help as loadHelp} from "./deprecated_load";
import {help as loadHelp} from "./load";
import {help as analyzeHelp} from "./analyze";
import {help as pagerankHelp} from "./pagerank";
import {help as scoresHelp} from "./scores";

View File

@ -4,8 +4,6 @@
import {toCompat, type Compatible} from "../util/compat";
import path from "path";
import fs from "fs-extra";
import * as RepoIdRegistry from "../core/repoIdRegistry";
import {repoIdToString, stringToRepoId, type RepoId} from "../core/repoId";
import dedent from "../util/dedent";
import type {Command} from "./command";
import * as Common from "./common";
@ -18,25 +16,25 @@ import {
import {DEFAULT_CRED_CONFIG} from "../plugins/defaultCredConfig";
import {userNodeType} from "../plugins/github/declaration";
import * as GN from "../plugins/github/nodes";
import {directoryForProjectId} from "../core/project_io";
const COMPAT_INFO = {type: "sourcecred/cli/scores", version: "0.1.0"};
function usage(print: (string) => void): void {
print(
dedent`\
usage: sourcecred scores REPO_ID [--help]
usage: sourcecred scores PROJECT_ID [--help]
Print the SourceCred user scores for a given REPO_ID.
Data must already be loaded for the given REPO_ID, using
'sourcecred load REPO_ID'
Print the SourceCred user scores for a given PROJECT_ID.
Data must already be loaded for the given PROJECT_ID, using
'sourcecred load PROJECT_ID'
REPO_ID refers to a GitHub repository in the form OWNER/NAME: for
example, torvalds/linux. The REPO_ID may be a "combined" repo as
created by the --output flag to sourcecred load.
PROJECT_ID refers to a project, as loaded by the \`load\` command.
Run \`sourcecred load --help\` for details.
Arguments:
REPO_ID
Already-loaded repository for which to load data.
PROJECT_ID
Already-loaded project for which to load data.
--help
Show this help message and exit, as 'sourcecred help scores'.
@ -70,7 +68,7 @@ export type ScoreOutput = Compatible<{|
|}>;
export const scores: Command = async (args, std) => {
let repoId: RepoId | null = null;
let projectId: string | null = null;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--help": {
@ -78,33 +76,28 @@ export const scores: Command = async (args, std) => {
return 0;
}
default: {
if (repoId != null) return die(std, "multiple repository IDs provided");
// Should be a repository.
repoId = stringToRepoId(args[i]);
if (projectId != null) return die(std, "multiple project IDs provided");
projectId = args[i];
break;
}
}
}
if (repoId == null) {
return die(std, "no repository ID provided");
if (projectId == null) {
return die(std, "no project ID provided");
}
const directory = Common.sourcecredDirectory();
const registry = RepoIdRegistry.getRegistry(directory);
if (RepoIdRegistry.getEntry(registry, repoId) == null) {
const repoIdStr = repoIdToString(repoId);
std.err(`fatal: repository ID ${repoIdStr} not loaded`);
std.err(`Try running \`sourcecred load ${repoIdStr}\` first.`);
const projectDirectory = directoryForProjectId(
projectId,
Common.sourcecredDirectory()
);
const credFile = path.join(projectDirectory, "cred.json");
if (!fs.existsSync(credFile)) {
std.err(`fatal: project ${projectId} not loaded`);
std.err(`Try running \`sourcecred load ${projectId}\` first.`);
return 1;
}
const credFile = path.join(
Common.sourcecredDirectory(),
"data",
repoIdToString(repoId),
"cred.json"
);
const credBlob = await fs.readFile(credFile);
const credJSON = JSON.parse(credBlob.toString());
const timelineCred = TimelineCred.fromJSON(credJSON, DEFAULT_CRED_CONFIG);

View File

@ -6,7 +6,7 @@ import type {Command} from "./command";
import {VERSION_SHORT} from "../core/version";
import help from "./help";
import load from "./deprecated_load";
import load from "./load";
import analyze from "./analyze";
import pagerank from "./pagerank";
import scores from "./scores";

View File

@ -12,7 +12,7 @@ function mockCommand(name) {
}
jest.mock("./help", () => mockCommand("help"));
jest.mock("./deprecated_load", () => mockCommand("load"));
jest.mock("./load", () => mockCommand("load"));
jest.mock("./analyze", () => mockCommand("analyze"));
jest.mock("./pagerank", () => mockCommand("pagerank"));
jest.mock("./clear", () => mockCommand("clear"));

View File

@ -2,19 +2,19 @@
import React from "react";
import type {Assets} from "../webutil/assets";
import type {RepoId} from "../core/repoId";
import {TimelineExplorer} from "./TimelineExplorer";
import {TimelineCred} from "../analysis/timeline/timelineCred";
import {declaration as githubDeclaration} from "../plugins/github/declaration";
import {DEFAULT_CRED_CONFIG} from "../plugins/defaultCredConfig";
import {encodeProjectId, type ProjectId} from "../core/project";
export type Props = {|
+assets: Assets,
+repoId: RepoId,
+projectId: string,
+loader: Loader,
|};
export type Loader = (assets: Assets, repoId: RepoId) => Promise<LoadResult>;
export type Loader = (assets: Assets, projectId: string) => Promise<LoadResult>;
export type LoadResult = Loading | LoadSuccess | LoadError;
export type Loading = {|+type: "LOADING"|};
@ -37,7 +37,7 @@ export class TimelineApp extends React.Component<Props, State> {
async load() {
const loadResult = await this.props.loader(
this.props.assets,
this.props.repoId
this.props.projectId
);
this.setState({loadResult});
}
@ -68,7 +68,7 @@ export class TimelineApp extends React.Component<Props, State> {
return (
<TimelineExplorer
initialTimelineCred={timelineCred}
repoId={this.props.repoId}
projectId={this.props.projectId}
declarations={[githubDeclaration]}
/>
);
@ -81,12 +81,12 @@ export class TimelineApp extends React.Component<Props, State> {
export async function defaultLoader(
assets: Assets,
repoId: RepoId
projectId: ProjectId
): Promise<LoadResult> {
async function fetchCred(): Promise<TimelineCred> {
const url = assets.resolve(
`api/v1/data/data/${repoId.owner}/${repoId.name}/cred.json`
);
console.error(">>>DEFAULTLOADER");
const encodedId = encodeProjectId(projectId);
const url = assets.resolve(`api/v1/data/projects/${encodedId}/cred.json`);
const response = await fetch(url);
if (!response.ok) {
return Promise.reject(response);

View File

@ -2,7 +2,6 @@
import React from "react";
import deepEqual from "lodash.isequal";
import {type RepoId} from "../core/repoId";
import {type PluginDeclaration} from "../analysis/pluginDeclaration";
import {type Weights, copy as weightsCopy} from "../analysis/weights";
import {
@ -14,7 +13,7 @@ import {WeightConfig} from "./weights/WeightConfig";
import {WeightsFileManager} from "./weights/WeightsFileManager";
export type Props = {
repoId: RepoId,
projectId: string,
initialTimelineCred: TimelineCred,
// TODO: Get this info from the TimelineCred
declarations: $ReadOnlyArray<PluginDeclaration>,
@ -35,7 +34,7 @@ export type State = {
* It basically wraps a TimelineCredView with some additional features and options:
* - allows changing the weights and re-calculating cred with new weights
* - allows saving/loading weights
* - displays the RepoId
* - displays the string
*/
export class TimelineExplorer extends React.Component<Props, State> {
constructor(props: Props) {
@ -110,13 +109,12 @@ export class TimelineExplorer extends React.Component<Props, State> {
re-compute cred
</button>
);
const {owner, name} = this.props.repoId;
return (
<div>
<div style={{marginTop: 30, display: "flex"}}>
<span style={{paddingLeft: 30}}>
cred for {owner}/{name}
<a href={`/prototype/${owner}/${name}/`}>(legacy)</a>
cred for {this.props.projectId}
<a href={`/prototype/${this.props.projectId}/`}>(legacy)</a>
</span>
<span style={{flexGrow: 1}} />
<button

View File

@ -7,7 +7,6 @@ import type {LocalStore} from "../../webutil/localStore";
import CheckedLocalStore from "../../webutil/checkedLocalStore";
import BrowserLocalStore from "../../webutil/browserLocalStore";
import Link from "../../webutil/Link";
import type {RepoId} from "../../core/repoId";
import {type NodeAddressT} from "../../core/graph";
import {declaration as githubDeclaration} from "../../plugins/github/declaration";
@ -31,7 +30,7 @@ const feedbackUrl =
export class AppPage extends React.Component<{|
+assets: Assets,
+repoId: RepoId,
+projectId: string,
|}> {
static _LOCAL_STORE = new CheckedLocalStore(
new BrowserLocalStore({
@ -44,7 +43,7 @@ export class AppPage extends React.Component<{|
const App = createApp(createStateTransitionMachine);
return (
<App
repoId={this.props.repoId}
projectId={this.props.projectId}
assets={this.props.assets}
localStore={AppPage._LOCAL_STORE}
/>
@ -55,7 +54,7 @@ export class AppPage extends React.Component<{|
type Props = {|
+assets: Assets,
+localStore: LocalStore,
+repoId: RepoId,
+projectId: string,
|};
type State = {|
appState: AppState,
@ -74,7 +73,7 @@ export function createApp(
constructor(props: Props) {
super(props);
this.state = {
appState: initialState(this.props.repoId),
appState: initialState(this.props.projectId),
weights: defaultWeights(),
};
this.stateTransitionMachine = createSTM(
@ -137,7 +136,6 @@ export function createApp(
const spacer = () => (
<span style={{display: "inline-block", width: 12}} />
);
const {owner, name} = this.props.repoId;
return (
<div style={{maxWidth: 900, margin: "0 auto", padding: "0 10px"}}>
<p style={{textAlign: "right"}}>
@ -147,10 +145,11 @@ export function createApp(
</p>
<h2>SourceCred Legacy Mode</h2>
<p>
Back to <a href={`/timeline/${owner}/${name}/`}>timeline mode</a>
Back to{" "}
<a href={`/timeline/${this.props.projectId}/`}>timeline mode</a>
</p>
<ProjectDetail repoId={this.props.repoId} />
<ProjectDetail projectId={this.props.projectId} />
<button
disabled={
appState.type === "UNINITIALIZED" ||
@ -179,10 +178,10 @@ export function createApp(
}
export class ProjectDetail extends React.PureComponent<{|
+repoId: RepoId,
+projectId: string,
|}> {
render() {
return <p>{`${this.props.repoId.owner}/${this.props.repoId.name}`}</p>;
return <p>{this.props.projectId}</p>;
}
}

View File

@ -4,7 +4,6 @@ import React from "react";
import {shallow} from "enzyme";
import {Graph} from "../../core/graph";
import {makeRepoId} from "../../core/repoId";
import {Assets} from "../../webutil/assets";
import testLocalStore from "../../webutil/testLocalStore";
@ -34,7 +33,7 @@ describe("explorer/legacy/App", () => {
<App
assets={new Assets("/foo/")}
localStore={localStore}
repoId={makeRepoId("foo", "bar")}
projectId={"foo/bar"}
/>
);
if (setState == null || getState == null) {
@ -55,14 +54,14 @@ describe("explorer/legacy/App", () => {
readyToLoadGraph: (loadingState) => {
return () => ({
type: "READY_TO_LOAD_GRAPH",
repoId: makeRepoId("foo", "bar"),
projectId: "foo/bar",
loading: loadingState,
});
},
readyToRunPagerank: (loadingState) => {
return () => ({
type: "READY_TO_RUN_PAGERANK",
repoId: makeRepoId("foo", "bar"),
projectId: "foo/bar",
loading: loadingState,
graph: new Graph(),
});
@ -70,7 +69,7 @@ describe("explorer/legacy/App", () => {
pagerankEvaluated: (loadingState) => {
return () => ({
type: "PAGERANK_EVALUATED",
repoId: makeRepoId("foo", "bar"),
projectId: "foo/bar",
loading: loadingState,
graph: new Graph(),
pagerankNodeDecomposition: new Map(),
@ -108,13 +107,12 @@ describe("explorer/legacy/App", () => {
it("instantiates a ProjectDetail component with correct props", () => {
const {el} = example();
const projectDetail = el.find(ProjectDetail);
const correctProps = {repoId: makeRepoId("foo", "bar")};
const correctProps = {projectId: "foo/bar"};
expect(projectDetail.props()).toEqual(correctProps);
});
it("ProjectDetail component renders repoId correctly", () => {
const repoId = makeRepoId("foo", "bar");
const projectDetail = shallow(<ProjectDetail repoId={repoId} />);
it("ProjectDetail component renders projectId correctly", () => {
const projectDetail = shallow(<ProjectDetail projectId={"foo/bar"} />);
const title = projectDetail.findWhere(
(p) => p.is("p") && p.text() === "foo/bar"
);

View File

@ -4,7 +4,6 @@ import deepEqual from "lodash.isequal";
import {Graph, type NodeAddressT} from "../../core/graph";
import type {Assets} from "../../webutil/assets";
import type {RepoId} from "../../core/repoId";
import {type EdgeEvaluator} from "../../analysis/pagerank";
import type {NodeAndEdgeTypes} from "../../analysis/types";
import {defaultLoader} from "../TimelineApp";
@ -33,25 +32,25 @@ export type AppState =
export type ReadyToLoadGraph = {|
+type: "READY_TO_LOAD_GRAPH",
+repoId: RepoId,
+projectId: string,
+loading: LoadingState,
|};
export type ReadyToRunPagerank = {|
+type: "READY_TO_RUN_PAGERANK",
+repoId: RepoId,
+projectId: string,
+graph: Graph,
+loading: LoadingState,
|};
export type PagerankEvaluated = {|
+type: "PAGERANK_EVALUATED",
+graph: Graph,
+repoId: RepoId,
+projectId: string,
+pagerankNodeDecomposition: PagerankNodeDecomposition,
+loading: LoadingState,
|};
export function initialState(repoId: RepoId): ReadyToLoadGraph {
return {type: "READY_TO_LOAD_GRAPH", repoId, loading: "NOT_LOADING"};
export function initialState(projectId: string): ReadyToLoadGraph {
return {type: "READY_TO_LOAD_GRAPH", projectId, loading: "NOT_LOADING"};
}
export function createStateTransitionMachine(
@ -79,7 +78,7 @@ export interface StateTransitionMachineInterface {
export class StateTransitionMachine implements StateTransitionMachineInterface {
getState: () => AppState;
setState: (AppState) => void;
doLoadGraph: (assets: Assets, repoId: RepoId) => Promise<Graph>;
doLoadGraph: (assets: Assets, projectId: string) => Promise<Graph>;
pagerank: (
Graph,
EdgeEvaluator,
@ -89,7 +88,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
constructor(
getState: () => AppState,
setState: (AppState) => void,
doLoadGraph: (assets: Assets, repoId: RepoId) => Promise<Graph>,
doLoadGraph: (assets: Assets, projectId: string) => Promise<Graph>,
pagerank: (
Graph,
EdgeEvaluator,
@ -108,17 +107,17 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
if (state.type !== "READY_TO_LOAD_GRAPH") {
throw new Error("Tried to loadGraph in incorrect state");
}
const {repoId} = state;
const {projectId} = state;
const loadingState = {...state, loading: "LOADING"};
this.setState(loadingState);
let newState: ?AppState;
let success = true;
try {
const graph = await this.doLoadGraph(assets, repoId);
const graph = await this.doLoadGraph(assets, projectId);
newState = {
type: "READY_TO_RUN_PAGERANK",
graph,
repoId,
projectId,
loading: "NOT_LOADING",
};
} catch (e) {
@ -166,7 +165,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
type: "PAGERANK_EVALUATED",
pagerankNodeDecomposition,
graph: state.graph,
repoId: state.repoId,
projectId: state.projectId,
loading: "NOT_LOADING",
};
} catch (e) {
@ -209,9 +208,9 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
export async function doLoadGraph(
assets: Assets,
repoId: RepoId
projectId: string
): Promise<Graph> {
const loadResult = await defaultLoader(assets, repoId);
const loadResult = await defaultLoader(assets, projectId);
if (loadResult.type !== "SUCCESS") {
throw new Error(loadResult);
}

View File

@ -4,7 +4,6 @@ import {StateTransitionMachine, type AppState} from "./state";
import {Graph, NodeAddress} from "../../core/graph";
import {Assets} from "../../webutil/assets";
import {makeRepoId, type RepoId} from "../../core/repoId";
import {type EdgeEvaluator} from "../../analysis/pagerank";
import {defaultWeights} from "../../analysis/weights";
import type {
@ -20,7 +19,7 @@ describe("explorer/legacy/state", () => {
stateContainer.appState = appState;
};
const loadGraphMock: JestMockFn<
[Assets, RepoId],
[Assets, string],
Promise<Graph>
> = jest.fn();
@ -39,14 +38,14 @@ describe("explorer/legacy/state", () => {
function readyToLoadGraph(): AppState {
return {
type: "READY_TO_LOAD_GRAPH",
repoId: makeRepoId("foo", "bar"),
projectId: "foo/bar",
loading: "NOT_LOADING",
};
}
function readyToRunPagerank(): AppState {
return {
type: "READY_TO_RUN_PAGERANK",
repoId: makeRepoId("foo", "bar"),
projectId: "foo/bar",
loading: "NOT_LOADING",
graph: new Graph(),
};
@ -54,7 +53,7 @@ describe("explorer/legacy/state", () => {
function pagerankEvaluated(): AppState {
return {
type: "PAGERANK_EVALUATED",
repoId: makeRepoId("foo", "bar"),
projectId: "foo/bar",
graph: new Graph(),
pagerankNodeDecomposition: pagerankNodeDecomposition(),
loading: "NOT_LOADING",
@ -80,16 +79,13 @@ describe("explorer/legacy/state", () => {
);
}
});
it("passes along the repoId", () => {
it("passes along the projectId", () => {
const {stm, loadGraphMock} = example(readyToLoadGraph());
expect(loadGraphMock).toHaveBeenCalledTimes(0);
const assets = new Assets("/my/gateway/");
stm.loadGraph(assets);
expect(loadGraphMock).toHaveBeenCalledTimes(1);
expect(loadGraphMock).toHaveBeenCalledWith(
assets,
makeRepoId("foo", "bar")
);
expect(loadGraphMock).toHaveBeenCalledWith(assets, "foo/bar");
});
it("immediately sets loading status", () => {
const {getState, stm} = example(readyToLoadGraph());

View File

@ -2,16 +2,17 @@
import React, {type ComponentType} from "react";
import type {RepoId} from "../core/repoId";
import type {Assets} from "../webutil/assets";
import HomepageExplorer from "./homepageExplorer";
export default function makeProjectPage(
repoId: RepoId
projectId: string
): ComponentType<{|+assets: Assets|}> {
return class ProjectPage extends React.Component<{|+assets: Assets|}> {
render() {
return <HomepageExplorer assets={this.props.assets} repoId={repoId} />;
return (
<HomepageExplorer assets={this.props.assets} projectId={projectId} />
);
}
};
}

View File

@ -1,14 +1,12 @@
// @flow
import stringify from "json-stable-stringify";
import React, {type ComponentType} from "react";
import type {RepoIdRegistry} from "../core/repoIdRegistry";
import Link from "../webutil/Link";
import type {Assets} from "../webutil/assets";
export default function makePrototypesPage(
registry: RepoIdRegistry
projectIds: $ReadOnlyArray<string>
): ComponentType<{|+assets: Assets|}> {
return class PrototypesPage extends React.Component<{|+assets: Assets|}> {
render() {
@ -24,11 +22,9 @@ export default function makePrototypesPage(
>
<p>Select a project:</p>
<ul>
{registry.map((x) => (
<li key={stringify(x)}>
<Link to={`/timeline/${x.repoId.owner}/${x.repoId.name}/`}>
{`${x.repoId.owner}/${x.repoId.name}`}
</Link>
{projectIds.map((projectId) => (
<li key={projectId}>
<Link to={`/timeline/${projectId}/`}>{`${projectId}`}</Link>
</li>
))}
</ul>

View File

@ -2,16 +2,17 @@
import React, {type ComponentType} from "react";
import type {RepoId} from "../core/repoId";
import type {Assets} from "../webutil/assets";
import HomepageTimeline from "./homepageTimeline";
export default function makeTimelinePage(
repoId: RepoId
projectId: string
): ComponentType<{|+assets: Assets|}> {
return class TimelinePage extends React.Component<{|+assets: Assets|}> {
render() {
return <HomepageTimeline assets={this.props.assets} repoId={repoId} />;
return (
<HomepageTimeline assets={this.props.assets} projectId={projectId} />
);
}
};
}

View File

@ -1,13 +1,12 @@
// @flow
import type {RepoIdRegistry} from "../core/repoIdRegistry";
import {type RouteData, makeRouteData} from "./routeData";
export default function createRouteDataFromEnvironment(): RouteData {
const raw = process.env.REPO_REGISTRY;
const raw = process.env.PROJECT_IDS;
if (raw == null) {
throw new Error("fatal: repo ID registry unset");
throw new Error("fatal: project IDs unset");
}
const registry: RepoIdRegistry = JSON.parse(raw);
return makeRouteData(registry);
const ids: $ReadOnlyArray<string> = JSON.parse(raw);
return makeRouteData(ids);
}

View File

@ -4,13 +4,14 @@ import React from "react";
import type {Assets} from "../webutil/assets";
import {AppPage} from "../explorer/legacy/App";
import type {RepoId} from "../core/repoId";
export default class HomepageExplorer extends React.Component<{|
+assets: Assets,
+repoId: RepoId,
+projectId: string,
|}> {
render() {
return <AppPage assets={this.props.assets} repoId={this.props.repoId} />;
return (
<AppPage assets={this.props.assets} projectId={this.props.projectId} />
);
}
}

View File

@ -4,17 +4,16 @@ import React from "react";
import type {Assets} from "../webutil/assets";
import {TimelineApp, defaultLoader} from "../explorer/TimelineApp";
import type {RepoId} from "../core/repoId";
export default class TimelineExplorer extends React.Component<{|
+assets: Assets,
+repoId: RepoId,
+projectId: string,
|}> {
render() {
return (
<TimelineApp
assets={this.props.assets}
repoId={this.props.repoId}
projectId={this.props.projectId}
loader={defaultLoader}
/>
);

View File

@ -56,7 +56,9 @@ function inspectionTestFor(name, component) /*: RouteDatum */ {
};
}
function makeRouteData(registry /*: RepoIdRegistry */) /*: RouteData */ {
function makeRouteData(
projectIds /*: $ReadOnlyArray<string> */
) /*: RouteData */ {
return [
{
path: "/",
@ -71,27 +73,27 @@ function makeRouteData(registry /*: RepoIdRegistry */) /*: RouteData */ {
path: "/prototype/",
contents: {
type: "PAGE",
component: () => require("./PrototypesPage").default(registry),
component: () => require("./PrototypesPage").default(projectIds),
},
title: "SourceCred prototype",
navTitle: "Prototype",
},
...registry.map((entry) => ({
path: `/prototype/${entry.repoId.owner}/${entry.repoId.name}/`,
...projectIds.map((id) => ({
path: `/prototype/${id}/`,
contents: {
type: "PAGE",
component: () => require("./ProjectPage").default(entry.repoId),
component: () => require("./ProjectPage").default(id),
},
title: `${entry.repoId.owner}/${entry.repoId.name} • SourceCred`,
title: `${id} • SourceCred`,
navTitle: null,
})),
...registry.map((entry) => ({
path: `/timeline/${entry.repoId.owner}/${entry.repoId.name}/`,
...projectIds.map((id) => ({
path: `/timeline/${id}/`,
contents: {
type: "PAGE",
component: () => require("./TimelinePage").default(entry.repoId),
component: () => require("./TimelinePage").default(id),
},
title: `${entry.repoId.owner}/${entry.repoId.name} • Timeline`,
title: `${id} • Timeline`,
navTitle: null,
})),
{

View File

@ -1,13 +1,12 @@
// @flow
import {stringToRepoId} from "../core/repoId";
import {makeRouteData} from "./routeData";
describe("homepage/routeData", () => {
function routeData() {
return makeRouteData([
{repoId: stringToRepoId("sourcecred/example-github")},
{repoId: stringToRepoId("sourcecred/sourcecred")},
"sourcecred/example-github",
"sourcecred/sourcecred",
]);
}

View File

@ -16,7 +16,7 @@ import {createRoutes} from "./createRoutes";
import {resolveRouteFromPath, resolveTitleFromPath} from "./routeData";
// Side effect for testing purposes
console.log(`REPO_REGISTRY: ${process.env.REPO_REGISTRY || "bad"}`);
console.log(`PROJECT_IDS: ${process.env.PROJECT_IDS || "bad"}`);
// NOTE: This is a side-effect at module load time.
const routeData = createRouteDataFromEnvironment();

View File

@ -9,7 +9,8 @@ import {declaration} from "./declaration";
import {RelationalView} from "./relationalView";
import {createGraph} from "./createGraph";
describe("plugins/github/analysisAdapter", () => {
// This file is deprecated, and will be removed shortly.
describe.skip("plugins/github/analysisAdapter", () => {
it("the loader provides the declaration", () => {
const loader = new BackendAdapterLoader();
expect(loader.declaration()).toEqual(declaration);