Remove old CLI (#1883)

This commit removes the old CLI, which is being replaced by the new
instance-system based approach (currently called cli2).

This also removes sharness tests on the old CLI, as well as the
associated snapshots. This is a little unfortunate since the GitHub
snapshot did provide some validation against changes to the plugin; we'd
do well to re-integrate such a system when adding testing to cli2.

Test plan: `yarn test --full` passes. Since this is just a removal,
there's not much that can go wrong.
This commit is contained in:
Dandelion Mané 2020-06-21 21:41:35 -07:00 committed by GitHub
parent 222f4a0738
commit 609d07e04c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1 additions and 5251 deletions

View File

@ -29,7 +29,6 @@ module.exports = {
// source file, and the key will be the filename of the bundled entry
// point within the build directory.
backendEntryPoints: {
sourcecred: resolveApp("src/cli/main.js"),
sc2: resolveApp("src/cli2/main.js"),
//
generateGithubGraphqlFlowTypes: resolveApp(

View File

@ -1 +0,0 @@
[{"type":"sourcecred/pluginDeclarations","version":"0.1.0"},[{"edgePrefix":"E\u0000sourcecred\u0000github\u0000","edgeTypes":[{"backwardName":"is authored by","defaultWeight":{"backwards":1,"forwards":0.5},"description":"Connects a GitHub account to a post that they authored.\n\nExamples of posts include issues, pull requests, and comments.\n","forwardName":"authors","prefix":"E\u0000sourcecred\u0000github\u0000AUTHORS\u0000"},{"backwardName":"has child","defaultWeight":{"backwards":0.25,"forwards":1},"description":"Connects a GitHub entity to its child entities.\n\nFor example, a Repository has Issues and Pull Requests as children, and a\nPull Request has comments and reviews as children.\n","forwardName":"has parent","prefix":"E\u0000sourcecred\u0000github\u0000HAS_PARENT\u0000"},{"backwardName":"is merged by","defaultWeight":{"backwards":1,"forwards":0.5},"description":"Connects a GitHub pull request to the Git commit that it merges.\n","forwardName":"merges","prefix":"E\u0000sourcecred\u0000github\u0000MERGED_AS\u0000"},{"backwardName":"is referenced by","defaultWeight":{"backwards":0,"forwards":1},"description":"Connects a GitHub post to an entity that it references.\n\nFor example, if you write a GitHub issue comment that says \"thanks\n@username for pull #1337\", it will create references edges to both the user\n@username, and to pull #1337 in the same repository.\n","forwardName":"references","prefix":"E\u0000sourcecred\u0000github\u0000REFERENCES\u0000"},{"backwardName":"got 👍 from","defaultWeight":{"backwards":0,"forwards":1},"description":"Connects users to posts to which they gave a 👍 reaction.\n","forwardName":"reacted 👍 to","prefix":"E\u0000sourcecred\u0000github\u0000REACTS\u0000THUMBS_UP\u0000"},{"backwardName":"got ❤️ from","defaultWeight":{"backwards":0,"forwards":2},"description":"Connects users to posts to which they gave a ❤️ reaction.\n","forwardName":"reacted ❤️ to","prefix":"E\u0000sourcecred\u0000github\u0000REACTS\u0000HEART\u0000"},{"backwardName":"got 🎉 from","defaultWeight":{"backwards":0,"forwards":4},"description":"Connects users to posts to which they gave a 🎉 reaction.\n","forwardName":"reacted 🎉 to","prefix":"E\u0000sourcecred\u0000github\u0000REACTS\u0000HOORAY\u0000"},{"backwardName":"got 🚀 from","defaultWeight":{"backwards":0,"forwards":1},"description":"Connects users to posts to which they gave a 🚀 reaction.\n","forwardName":"reacted 🚀 to","prefix":"E\u0000sourcecred\u0000github\u0000REACTS\u0000ROCKET\u0000"},{"backwardName":"merged on GitHub as","defaultWeight":{"backwards":1,"forwards":1},"description":"Connects a commit on GitHub to the corresponding raw Git commit.\n","forwardName":"corresponds to Git commit","prefix":"E\u0000sourcecred\u0000github\u0000CORRESPONDS_TO_COMMIT_TYPE\u0000"}],"name":"GitHub","nodePrefix":"N\u0000sourcecred\u0000github\u0000","nodeTypes":[{"defaultWeight":4,"description":"NodeType for a GitHub repository","name":"Repository","pluralName":"Repositories","prefix":"N\u0000sourcecred\u0000github\u0000REPO\u0000"},{"defaultWeight":2,"description":"NodeType for a GitHub issue","name":"Issue","pluralName":"Issues","prefix":"N\u0000sourcecred\u0000github\u0000ISSUE\u0000"},{"defaultWeight":4,"description":"NodeType for a GitHub pull request","name":"Pull request","pluralName":"Pull requests","prefix":"N\u0000sourcecred\u0000github\u0000PULL\u0000"},{"defaultWeight":1,"description":"NodeType for a GitHub code review","name":"Pull request review","pluralName":"Pull request reviews","prefix":"N\u0000sourcecred\u0000github\u0000REVIEW\u0000"},{"defaultWeight":1,"description":"NodeType for a GitHub comment","name":"Comment","pluralName":"Comments","prefix":"N\u0000sourcecred\u0000github\u0000COMMENT\u0000"},{"defaultWeight":1,"description":"Represents a particular Git commit on GitHub, i.e. scoped to a particular repository","name":"Commit","pluralName":"Commits","prefix":"N\u0000sourcecred\u0000github\u0000COMMIT\u0000"},{"defaultWeight":0,"description":"NodeType for a GitHub user","name":"User","pluralName":"Users","prefix":"N\u0000sourcecred\u0000github\u0000USERLIKE\u0000USER\u0000"},{"defaultWeight":0,"description":"NodeType for a GitHub bot account","name":"Bot","pluralName":"Bots","prefix":"N\u0000sourcecred\u0000github\u0000USERLIKE\u0000BOT\u0000"}],"userTypes":[{"defaultWeight":0,"description":"NodeType for a GitHub user","name":"User","pluralName":"Users","prefix":"N\u0000sourcecred\u0000github\u0000USERLIKE\u0000USER\u0000"}]}]]

View File

@ -1 +0,0 @@
[{"type":"sourcecred/project","version":"0.5.2"},{"discord":null,"discourseServer":null,"id":"sourcecred-test/example-github","identities":[],"initiatives":null,"repoIds":[{"name":"example-github","owner":"sourcecred-test"}],"timelineCredParams":null}]

File diff suppressed because it is too large Load Diff

View File

@ -1,324 +0,0 @@
[
{
"type": "sourcecred/cli/scores",
"version": "0.2.0"
},
{
"intervals": [
{
"endTimeMs": 1520121600000,
"startTimeMs": 1519516800000
},
{
"endTimeMs": 1520726400000,
"startTimeMs": 1520121600000
},
{
"endTimeMs": 1521331200000,
"startTimeMs": 1520726400000
},
{
"endTimeMs": 1521936000000,
"startTimeMs": 1521331200000
},
{
"endTimeMs": 1522540800000,
"startTimeMs": 1521936000000
},
{
"endTimeMs": 1523145600000,
"startTimeMs": 1522540800000
},
{
"endTimeMs": 1523750400000,
"startTimeMs": 1523145600000
},
{
"endTimeMs": 1524355200000,
"startTimeMs": 1523750400000
},
{
"endTimeMs": 1524960000000,
"startTimeMs": 1524355200000
},
{
"endTimeMs": 1525564800000,
"startTimeMs": 1524960000000
},
{
"endTimeMs": 1526169600000,
"startTimeMs": 1525564800000
},
{
"endTimeMs": 1526774400000,
"startTimeMs": 1526169600000
},
{
"endTimeMs": 1527379200000,
"startTimeMs": 1526774400000
},
{
"endTimeMs": 1527984000000,
"startTimeMs": 1527379200000
},
{
"endTimeMs": 1528588800000,
"startTimeMs": 1527984000000
},
{
"endTimeMs": 1529193600000,
"startTimeMs": 1528588800000
},
{
"endTimeMs": 1529798400000,
"startTimeMs": 1529193600000
},
{
"endTimeMs": 1530403200000,
"startTimeMs": 1529798400000
},
{
"endTimeMs": 1531008000000,
"startTimeMs": 1530403200000
},
{
"endTimeMs": 1531612800000,
"startTimeMs": 1531008000000
},
{
"endTimeMs": 1532217600000,
"startTimeMs": 1531612800000
},
{
"endTimeMs": 1532822400000,
"startTimeMs": 1532217600000
},
{
"endTimeMs": 1533427200000,
"startTimeMs": 1532822400000
},
{
"endTimeMs": 1534032000000,
"startTimeMs": 1533427200000
},
{
"endTimeMs": 1534636800000,
"startTimeMs": 1534032000000
},
{
"endTimeMs": 1535241600000,
"startTimeMs": 1534636800000
},
{
"endTimeMs": 1535846400000,
"startTimeMs": 1535241600000
},
{
"endTimeMs": 1536451200000,
"startTimeMs": 1535846400000
},
{
"endTimeMs": 1537056000000,
"startTimeMs": 1536451200000
},
{
"endTimeMs": 1537660800000,
"startTimeMs": 1537056000000
},
{
"endTimeMs": 1538265600000,
"startTimeMs": 1537660800000
},
{
"endTimeMs": 1538870400000,
"startTimeMs": 1538265600000
},
{
"endTimeMs": 1539475200000,
"startTimeMs": 1538870400000
},
{
"endTimeMs": 1540080000000,
"startTimeMs": 1539475200000
},
{
"endTimeMs": 1540684800000,
"startTimeMs": 1540080000000
},
{
"endTimeMs": 1541289600000,
"startTimeMs": 1540684800000
},
{
"endTimeMs": 1541894400000,
"startTimeMs": 1541289600000
},
{
"endTimeMs": 1542499200000,
"startTimeMs": 1541894400000
},
{
"endTimeMs": 1543104000000,
"startTimeMs": 1542499200000
},
{
"endTimeMs": 1543708800000,
"startTimeMs": 1543104000000
},
{
"endTimeMs": 1544313600000,
"startTimeMs": 1543708800000
},
{
"endTimeMs": 1544918400000,
"startTimeMs": 1544313600000
},
{
"endTimeMs": 1545523200000,
"startTimeMs": 1544918400000
},
{
"endTimeMs": 1546128000000,
"startTimeMs": 1545523200000
},
{
"endTimeMs": 1546732800000,
"startTimeMs": 1546128000000
},
{
"endTimeMs": 1547337600000,
"startTimeMs": 1546732800000
},
{
"endTimeMs": 1547942400000,
"startTimeMs": 1547337600000
},
{
"endTimeMs": 1548547200000,
"startTimeMs": 1547942400000
}
],
"users": [
{
"address": [
"sourcecred",
"github",
"USERLIKE",
"USER",
"decentralion"
],
"intervalCred": [
9.734371505131229,
4.866769476502015,
5.45967678251199,
4.7373704438615425,
2.3682738955453986,
1.183726718845694,
0.5914613180048877,
0.2953441802319966,
0.1473144899165441,
3.4978536212952984,
1.7559491655988428,
0.8839962777883124,
0.44653137513507696,
0.22600614078858708,
0.11412708331142937,
0.04241671830125515,
0.02095600332095804,
0.5492132258029613,
0.27337032661021154,
0.13551003711598433,
0.06682765923330519,
0.03282560139637494,
0.01614582900556915,
0.008011848698948467,
0.0040263763117773495,
0.002045146244647576,
0.234125107916456,
0.11935025856951863,
2.066716441640751,
1.225987646747624,
0.6136508223713587,
0.3070637150934105,
0.15361287410473679,
0.07683553891284155,
0.03842818491166919,
0.019214582306587714,
0.009601140978431138,
0.004791390002030775,
0.002387368453447827,
0.0011885801142664197,
0.0005920728445612202,
0.00029490583043628775,
0.00014677639527190518,
0.000073579877320932,
0.00003767874911054204,
0.000019748431956321648,
0.000010429797539164292,
0.0000021162129948814497
],
"totalCred": 42.33425220677316
},
{
"address": [
"sourcecred",
"github",
"USERLIKE",
"USER",
"wchargin"
],
"intervalCred": [
3.2656284948687713,
1.6332305234979851,
0.7903232174880094,
0.387629556138458,
0.19422610445460123,
0.0975232811543062,
0.049163681995112264,
0.024968319768003445,
0.012841760083455873,
2.0822245037047016,
1.034089896901157,
0.5110232534616876,
0.2509783904899231,
0.12274874202391291,
0.06025035809482062,
0.5447720024018698,
0.27263835703060446,
0.59758395437282,
0.3000282634776791,
0.15118925792796095,
0.07652198828866748,
0.03884922236461138,
0.01969158287492401,
0.009906857241298115,
0.004932976658345941,
0.002434530240414069,
0.2681147303260748,
0.13176966055174677,
4.058843517919881,
1.8367923330326925,
0.9177391675187996,
0.4586312798516685,
0.22923462336780273,
0.11458820982342821,
0.05728368945646571,
0.02864135487747973,
0.014326827613602582,
0.007172594293986085,
0.003594623694560603,
0.0018024159597377954,
0.0009034251924408875,
0.00045284318806476605,
0.00022709811397862172,
0.00011335737730433143,
0.00005578987820208967,
0.000026985881699994214,
0.00001293735928899364,
0.000009567365419197517
],
"totalCred": 20.665736109648424
}
]
}
]

View File

@ -1,96 +0,0 @@
#!/bin/sh
# Disable these lint rules globally:
# 2034 = unused variable (used by sharness)
# 2016 = parameter expansion in single quotes
# 1004 = backslash-newline in single quotes
# shellcheck disable=SC2034,SC2016,SC1004
:
test_description='tests for cli/output.js'
export GIT_CONFIG_NOSYSTEM=1
export GIT_ATTR_NOSYSTEM=1
# shellcheck disable=SC1091
. ./sharness.sh
test_expect_success "environment and Node linking setup" '
toplevel="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)" &&
snapshot_directory="${toplevel}/sharness/__snapshots__/" &&
SOURCECRED_DIRECTORY="${snapshot_directory}/example-github-load" &&
export SOURCECRED_DIRECTORY &&
snapshot_file="${snapshot_directory}/example-github-output.json" &&
if [ -z "${SOURCECRED_BIN}" ]; then
printf >&2 "warn: missing environment variable SOURCECRED_BIN\n" &&
printf >&2 "warn: using repository bin directory as fallback\n" &&
export SOURCECRED_BIN="${toplevel}/bin"
fi &&
export NODE_PATH="${toplevel}/node_modules${NODE_PATH:+:${NODE_PATH}}" &&
test_set_prereq SETUP
'
run() (
set -eu
rm -f out err
code=0
node "${SOURCECRED_BIN}"/sourcecred.js "$@" >out 2>err || code=$?
if [ "${code}" -ne 0 ]; then
printf '%s failed with %d\n' "sourcecred $*"
printf 'stdout:\n'
cat out
printf 'stderr:\n'
cat err
fi
)
# Use this instead of `run` when we are expecting sourcecred to return a
# non-zero exit code
run_without_validation() (
set -eu
rm -f out err
node "${SOURCECRED_BIN}"/sourcecred.js "$@" >out 2>err
)
test_expect_success SETUP "should print help message when called without args" '
test_must_fail run_without_validation output &&
grep -q "no project ID provided" err &&
grep -q "sourcecred help output" err
'
test_expect_success SETUP "help should print usage info" '
run help output &&
grep -q "usage: sourcecred output PROJECT_ID" out
'
test_expect_success SETUP "--help should print usage info" '
run output --help &&
grep -q "usage: sourcecred output PROJECT_ID" out
'
test_expect_success SETUP "should fail for multiple projects" '
test_must_fail run_without_validation output sourcecred/sourcecred torvalds/linux &&
grep -q "fatal: multiple project IDs provided" err
'
test_expect_success SETUP "should fail for unloaded project" '
test_must_fail run_without_validation output torvalds/linux &&
grep -q "fatal: project torvalds/linux not loaded" err
'
if [ -n "${UPDATE_SNAPSHOT}" ]; then
test_set_prereq UPDATE_SNAPSHOT
fi
test_expect_success SETUP,UPDATE_SNAPSHOT "should update the snapshot" '
run output sourcecred-test/example-github &&
mv out "${snapshot_file}"
'
test_expect_success SETUP "should be identical to the snapshot" '
run output sourcecred-test/example-github &&
diff -u out ${snapshot_file}
'
test_done
# vim: ft=sh

View File

@ -1,96 +0,0 @@
#!/bin/sh
# Disable these lint rules globally:
# 2034 = unused variable (used by sharness)
# 2016 = parameter expansion in single quotes
# 1004 = backslash-newline in single quotes
# shellcheck disable=SC2034,SC2016,SC1004
:
test_description='tests for cli/scores.js'
export GIT_CONFIG_NOSYSTEM=1
export GIT_ATTR_NOSYSTEM=1
# shellcheck disable=SC1091
. ./sharness.sh
test_expect_success "environment and Node linking setup" '
toplevel="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)" &&
snapshot_directory="${toplevel}/sharness/__snapshots__/" &&
SOURCECRED_DIRECTORY="${snapshot_directory}/example-github-load" &&
export SOURCECRED_DIRECTORY &&
snapshot_file="${snapshot_directory}/example-github-scores.json" &&
if [ -z "${SOURCECRED_BIN}" ]; then
printf >&2 "warn: missing environment variable SOURCECRED_BIN\n" &&
printf >&2 "warn: using repository bin directory as fallback\n" &&
export SOURCECRED_BIN="${toplevel}/bin"
fi &&
export NODE_PATH="${toplevel}/node_modules${NODE_PATH:+:${NODE_PATH}}" &&
test_set_prereq SETUP
'
run() (
set -eu
rm -f out err
code=0
node "${SOURCECRED_BIN}"/sourcecred.js "$@" >out 2>err || code=$?
if [ "${code}" -ne 0 ]; then
printf '%s failed with %d\n' "sourcecred $*"
printf 'stdout:\n'
cat out
printf 'stderr:\n'
cat err
fi
)
# Use this instead of `run` when we are expecting sourcecred to return a
# non-zero exit code
run_without_validation() (
set -eu
rm -f out err
node "${SOURCECRED_BIN}"/sourcecred.js "$@" >out 2>err
)
test_expect_success SETUP "should print help message when called without args" '
test_must_fail run_without_validation scores &&
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 PROJECT_ID" out
'
test_expect_success SETUP "--help should print usage info" '
run scores --help &&
grep -q "usage: sourcecred scores PROJECT_ID" out
'
test_expect_success SETUP "should fail for multiple projects" '
test_must_fail run_without_validation scores sourcecred/sourcecred torvalds/linux &&
grep -q "fatal: multiple project IDs provided" err
'
test_expect_success SETUP "should fail for unloaded project" '
test_must_fail run_without_validation scores torvalds/linux &&
grep -q "fatal: project torvalds/linux not loaded" err
'
if [ -n "${UPDATE_SNAPSHOT}" ]; then
test_set_prereq UPDATE_SNAPSHOT
fi
test_expect_success SETUP,UPDATE_SNAPSHOT "should update the snapshot" '
run scores sourcecred-test/example-github &&
mv out "${snapshot_file}"
'
test_expect_success SETUP "should be identical to the snapshot" '
run scores sourcecred-test/example-github &&
diff -u out ${snapshot_file}
'
test_done
# vim: ft=sh

View File

@ -1,62 +0,0 @@
#!/bin/sh
# Disable these lint rules globally:
# 2034 = unused variable (used by sharness)
# 2016 = parameter expansion in single quotes
# 1004 = backslash-newline in single quotes
# shellcheck disable=SC2034,SC2016,SC1004
:
# If this test is failing, it probably means you need to update snapshots.
# You can do so by setting your SOURCECRED_GITHUB_TOKEN (see README) and then
# running scripts/update_snapshots.sh.
test_description='test snapshot integrity for sourcecred load'
export GIT_CONFIG_NOSYSTEM=1
export GIT_ATTR_NOSYSTEM=1
# shellcheck disable=SC1091
. ./sharness.sh
test_expect_success "environment and Node linking setup" '
toplevel="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)" &&
snapshot_directory="${toplevel}/sharness/__snapshots__/example-github-load" &&
if [ -z "${SOURCECRED_BIN}" ]; then
printf >&2 "warn: missing environment variable SOURCECRED_BIN\n" &&
printf >&2 "warn: using repository bin directory as fallback\n" &&
export SOURCECRED_BIN="${toplevel}/bin"
fi &&
export NODE_PATH="${toplevel}/node_modules${NODE_PATH:+:${NODE_PATH}}" &&
test_set_prereq SETUP
'
if [ -n "${SOURCECRED_GITHUB_TOKEN:-}" ]; then
test_set_prereq HAVE_GITHUB_TOKEN
fi
test_expect_success EXPENSIVE,SETUP,HAVE_GITHUB_TOKEN \
"should load sourcecred-test/example-github" '
SOURCECRED_DIRECTORY=. node "${SOURCECRED_BIN}/sourcecred.js" \
load sourcecred-test/example-github &&
rm -rf cache &&
test_set_prereq LOADED_GITHUB
'
if [ -n "${UPDATE_SNAPSHOT}" ]; then
test_set_prereq UPDATE_SNAPSHOT
fi
test_expect_success LOADED_GITHUB,UPDATE_SNAPSHOT \
"should update the snapshot" '
rm -rf "$snapshot_directory" &&
cp -r . "$snapshot_directory"
'
test_expect_success LOADED_GITHUB "should be identical to the snapshot" '
diff -qr . "$snapshot_directory"
'
test_done
# vim: ft=sh

View File

@ -1,111 +0,0 @@
// @flow
// implementation of `sourcecred clear`
import path from "path";
import rimraf from "rimraf";
import dedent from "../util/dedent";
import {type Command} from "./command";
import * as Common from "./common";
function usage(print: (string) => void): void {
print(
dedent`\
usage: sourcecred clear --all
sourcecred clear --cache
sourcecred clear --help
Remove the SOURCECRED_DIRECTORY, i.e. the directory where data, caches,
registries, etc. owned by SourceCred are stored.
Arguments:
--all
remove entire SOURCECRED_DIRECTORY
--cache
remove only the SourcCred cache directory
--help
Show this help message and exit, as 'sourcecred help clear'.
Environment Variables:
SOURCECRED_DIRECTORY
Directory owned by SourceCred, in which data, caches,
registries, etc. are stored. Optional: defaults to a
directory 'sourcecred' under your OS's temporary directory;
namely:
${Common.defaultSourcecredDirectory()}
`.trimRight()
);
}
function die(std, message) {
std.err("fatal: " + message);
std.err("fatal: run 'sourcecred help clear' for help");
return 1;
}
export function makeClear(removeDir: (string) => Promise<void>): Command {
return async function clear(args, std) {
async function remove(dir) {
try {
await removeDir(dir);
return 0;
} catch (error) {
return die(std, `${error}`);
}
}
switch (args.length) {
case 0:
return die(std, "no arguments provided");
case 1:
switch (args[0]) {
case "--help":
usage(std.out);
return 0;
case "--all":
return remove(Common.sourcecredDirectory());
case "--cache":
return remove(path.join(Common.sourcecredDirectory(), "cache"));
default:
return die(std, `unrecognized argument: '${args[0]}'`);
}
default:
return die(
std,
`expected 1 argument but recieved: ${args.length} arguments`
);
}
};
}
export function removeDir(p: string): Promise<void> {
return new Promise((resolve, reject) =>
rimraf(p, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
})
);
}
export const help: Command = async (args, std) => {
if (args.length === 0) {
usage(std.out);
return 0;
} else {
usage(std.err);
return 1;
}
};
export const clear = makeClear(removeDir);
export default clear;

View File

@ -1,134 +0,0 @@
// @flow
import path from "path";
import tmp from "tmp";
import fs from "fs";
import {makeClear, removeDir, help} from "./clear";
import {run} from "./testUtil";
import * as Common from "./common";
describe("cli/clear", () => {
describe("'help' command", () => {
it("prints usage when given no arguments", async () => {
expect(await run(help, [])).toEqual({
exitCode: 0,
stdout: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred clear/),
]),
stderr: [],
});
});
it("fails when given arguments", async () => {
expect(await run(help, ["foo/bar"])).toEqual({
exitCode: 1,
stdout: [],
stderr: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred clear/),
]),
});
});
});
describe("'makeClear' command", () => {
it("prints usage with '--help'", async () => {
const clear = makeClear(jest.fn());
expect(await run(clear, ["--help"])).toEqual({
exitCode: 0,
stdout: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred clear/),
]),
stderr: [],
});
});
it("fails when no arguments specified", async () => {
const clear = makeClear(jest.fn());
expect(await run(clear, [])).toEqual({
exitCode: 1,
stdout: [],
stderr: [
"fatal: no arguments provided",
"fatal: run 'sourcecred help clear' for help",
],
});
});
it("fails when an invalid argument is specified", async () => {
const clear = makeClear(jest.fn());
expect(await run(clear, ["invalid"])).toEqual({
exitCode: 1,
stdout: [],
stderr: [
"fatal: unrecognized argument: 'invalid'",
"fatal: run 'sourcecred help clear' for help",
],
});
});
it("fails when more than one argument specified", async () => {
const clear = makeClear(jest.fn());
expect(await run(clear, ["1", "2"])).toEqual({
exitCode: 1,
stdout: [],
stderr: [
"fatal: expected 1 argument but recieved: 2 arguments",
"fatal: run 'sourcecred help clear' for help",
],
});
});
it("passes correct param to removeDir with `--all`", async () => {
const rmDir = jest.fn();
const clear = makeClear(rmDir);
await run(clear, ["--all"]);
expect(rmDir).toHaveBeenCalledWith(Common.sourcecredDirectory());
});
it("passes correct param to removeDir with `--cache`", async () => {
const rmDir = jest.fn();
const clear = makeClear(rmDir);
await run(clear, ["--cache"]);
const cacheDir = path.join(Common.sourcecredDirectory(), "cache");
expect(rmDir).toHaveBeenCalledWith(cacheDir);
});
function throwError() {
return Promise.reject(new Error("test error"));
}
it("--all returns error if removeDir errors", async () => {
const clear = makeClear(throwError);
expect(await run(clear, ["--all"])).toEqual({
exitCode: 1,
stdout: [],
stderr: [
"fatal: Error: test error",
"fatal: run 'sourcecred help clear' for help",
],
});
});
it("--cache returns error if removeDir errors", async () => {
const clear = makeClear(throwError);
expect(await run(clear, ["--cache"])).toEqual({
exitCode: 1,
stdout: [],
stderr: [
"fatal: Error: test error",
"fatal: run 'sourcecred help clear' for help",
],
});
});
});
describe("rimraf", () => {
it("removes the correct directory", async () => {
const dir = tmp.dirSync();
expect(fs.existsSync(dir.name)).toBe(true);
await removeDir(dir.name);
expect(fs.existsSync(dir.name)).toBe(false);
});
});
});

View File

@ -1,52 +0,0 @@
// @flow
// Configuration and environment variables used by the CLI.
import os from "os";
import path from "path";
import deepFreeze from "deep-freeze";
import fs from "fs-extra";
import {type Weights, fromJSON as weightsFromJSON} from "../core/weights";
import {validateToken, type GithubToken} from "../plugins/github/token";
import {type DiscordToken} from "../plugins/experimental-discord/config";
export type PluginName = "git" | "github";
export const defaultPlugins: PluginName[] = deepFreeze(["github"]);
export function defaultSourcecredDirectory() {
return path.join(os.tmpdir(), "sourcecred");
}
export function sourcecredDirectory(): string {
const env = process.env.SOURCECRED_DIRECTORY;
return env != null ? env : defaultSourcecredDirectory();
}
export function initiativesDirectory(): string | null {
return process.env.SOURCECRED_INITIATIVES_DIRECTORY || null;
}
export function githubToken(): ?GithubToken {
const envToken = process.env.SOURCECRED_GITHUB_TOKEN;
if (envToken == null || !envToken.length) {
return null;
}
return validateToken(envToken);
}
export function discordToken(): ?DiscordToken {
return process.env.SOURCECRED_DISCORD_TOKEN || null;
}
export async function loadWeights(path: string): Promise<Weights> {
if (!(await fs.exists(path))) {
throw new Error("Could not find the weights file");
}
const raw = await fs.readFile(path, "utf-8");
const weightsJSON = JSON.parse(raw);
try {
return weightsFromJSON(weightsJSON);
} catch (e) {
throw new Error(`provided weights file is invalid:\n${e}`);
}
}

View File

@ -1,110 +0,0 @@
// @flow
import path from "path";
import tmp from "tmp";
import fs from "fs-extra";
import * as Weights from "../core/weights";
import {NodeAddress} from "../core/graph";
import {validateToken} from "../plugins/github/token";
import {
defaultPlugins,
defaultSourcecredDirectory,
sourcecredDirectory,
githubToken,
loadWeights,
initiativesDirectory,
} from "./common";
describe("cli/common", () => {
const exampleGithubToken = validateToken("0".repeat(40));
const exampleInitiativesDirectory = path.join(__dirname, "initiatives");
beforeEach(() => {
jest
.spyOn(require("os"), "tmpdir")
.mockReturnValue(path.join("/", "your", "tmpdir"));
});
describe("defaultPlugins", () => {
it("is an array including the GitHub plugin name", () => {
expect(defaultPlugins).toEqual(expect.arrayContaining(["github"]));
});
});
describe("defaultSourcecredDirectory", () => {
it("gives a file under the OS's temporary directory", () => {
expect(defaultSourcecredDirectory()).toEqual(
path.join("/", "your", "tmpdir", "sourcecred")
);
});
});
describe("sourcecredDirectory", () => {
it("uses the environment variable when available", () => {
const dir = path.join("/", "my", "sourcecred");
process.env.SOURCECRED_DIRECTORY = dir;
expect(sourcecredDirectory()).toEqual(dir);
});
it("uses the default directory if no environment variable is set", () => {
delete process.env.SOURCECRED_DIRECTORY;
expect(sourcecredDirectory()).toEqual(
path.join("/", "your", "tmpdir", "sourcecred")
);
});
});
describe("githubToken", () => {
it("uses the environment variable when available", () => {
process.env.SOURCECRED_GITHUB_TOKEN = exampleGithubToken;
expect(githubToken()).toEqual(exampleGithubToken);
});
it("returns `null` if the environment variable is not set", () => {
delete process.env.SOURCECRED_GITHUB_TOKEN;
expect(githubToken()).toBe(null);
});
});
describe("initiativesDirectory", () => {
it("uses the environment variable when available", () => {
process.env.SOURCECRED_INITIATIVES_DIRECTORY = exampleInitiativesDirectory;
expect(initiativesDirectory()).toEqual(exampleInitiativesDirectory);
});
it("returns `null` if the environment variable is not set", () => {
delete process.env.SOURCECRED_INITIATIVES_DIRECTORY;
expect(initiativesDirectory()).toBe(null);
});
});
describe("loadWeights", () => {
function tmpWithContents(contents: mixed) {
const name = tmp.tmpNameSync();
fs.writeFileSync(name, JSON.stringify(contents));
return name;
}
it("works in a simple success case", async () => {
const weights = Weights.empty();
// Make a modification, just to be sure we aren't always loading the
// default weights.
weights.nodeWeights.set(NodeAddress.empty, 3);
const weightsJSON = Weights.toJSON(weights);
const file = tmpWithContents(weightsJSON);
const weights_ = await loadWeights(file);
expect(weights).toEqual(weights_);
});
it("rejects if the file is not a valid weights file", () => {
const file = tmpWithContents(1234);
expect.assertions(1);
return loadWeights(file).catch((e) =>
expect(e.message).toMatch("provided weights file is invalid:")
);
});
it("rejects if the file does not exist", () => {
const file = tmp.tmpNameSync();
expect.assertions(1);
return loadWeights(file).catch((e) =>
expect(e.message).toMatch("Could not find the weights file")
);
});
});
});

View File

@ -1,42 +0,0 @@
// @flow
import * as Common from "./common";
import {type Command} from "./command";
import {LoggingTaskReporter} from "../util/taskReporter";
import {DataDirectory} from "../backend/dataDirectory";
import Loader from "../plugins/experimental-discord/loader";
function die(std, message) {
std.err("fatal: " + message);
std.err("fatal: run 'sourcecred help discord' for help");
return 1;
}
// TODO: hack
const reactionWeights = {"sourcecred:678399364418502669": 4};
const discord: Command = async (args, std) => {
if (args.length !== 1) {
return die(std, "Expected one positional argument (or --help).");
}
const [guildId] = args;
const taskReporter = new LoggingTaskReporter();
const dir = new DataDirectory(Common.sourcecredDirectory());
const token = process.env.SOURCECRED_DISCORD_TOKEN || null;
if (!token) {
throw new Error("Expecting a SOURCECRED_DISCORD_TOKEN");
}
const opts = {
guildId,
reactionWeights,
};
await Loader.updateMirror(opts, token, dir, taskReporter);
const wg = await Loader.createGraph(opts, dir);
console.log(wg.graph, wg.weights);
return 0;
};
export default discord;

View File

@ -1,123 +0,0 @@
// @flow
// Implementation of `sourcecred discourse`
// This is a (likely temporary command) to facilitate loading a single
// discourse server.
import dedent from "../util/dedent";
import {LoggingTaskReporter} from "../util/taskReporter";
import type {Command} from "./command";
import * as Common from "./common";
import * as Weights from "../core/weights";
import {load} from "../api/load";
import {declaration as discourseDeclaration} from "../plugins/discourse/declaration";
import {type Project, createProject} from "../core/project";
function usage(print: (string) => void): void {
print(
dedent`\
usage: sourcecred discourse DISCOURSE_URL
[--weights WEIGHTS_FILE]
sourcecred discourse --help
Loads a target Discourse server, generating cred scores for it.
Arguments:
DISCOURSE_URL
The url to the Discourse server in question, for example
https://discourse.sourcecred.io
--weights WEIGHTS_FILE
Path to a json file which contains a weights configuration.
This will be used instead of the default weights and persisted.
--help
Show this help message and exit, as 'sourcecred help discourse'.
Environment variables:
SOURCECRED_DIRECTORY
Directory owned by SourceCred, in which data, caches,
registries, etc. are stored. Optional: defaults to a
directory 'sourcecred' under your OS's temporary directory;
namely:
${Common.defaultSourcecredDirectory()}
`.trimRight()
);
}
function die(std, message) {
std.err("fatal: " + message);
std.err("fatal: run 'sourcecred help discourse' for help");
return 1;
}
const command: Command = async (args, std) => {
const positionalArgs = [];
let weightsPath: string | null = null;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--help": {
usage(std.out);
return 0;
}
case "--weights": {
if (weightsPath != null)
return die(std, "'--weights' given multiple times");
if (++i >= args.length)
return die(std, "'--weights' given without value");
weightsPath = args[i];
break;
}
default: {
positionalArgs.push(args[i]);
break;
}
}
}
if (positionalArgs.length !== 1) {
return die(std, "Expected one positional arguments (or --help).");
}
const [serverUrl] = positionalArgs;
const httpRE = new RegExp(/^https?:\/\//);
if (!httpRE.test(serverUrl)) {
die(std, "expected server url to start with 'https://' or 'http://'");
}
const projectId = serverUrl.trim().replace(httpRE, "");
const project: Project = createProject({
id: projectId,
discourseServer: {serverUrl},
});
const taskReporter = new LoggingTaskReporter();
let weights = Weights.empty();
if (weightsPath) {
weights = await Common.loadWeights(weightsPath);
}
const plugins = [discourseDeclaration];
await load(
{
project,
params: null,
weightsOverrides: weights,
plugins,
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: null,
discordToken: null,
initiativesDirectory: null,
},
taskReporter
);
return 0;
};
export const help: Command = async (args, std) => {
if (args.length === 0) {
usage(std.out);
return 0;
} else {
usage(std.err);
return 1;
}
};
export default command;

View File

@ -1,163 +0,0 @@
// @flow
// Implementation of `sourcecred gen-project`.
// This method is intended as a placeholder for generating a project definition,
// before we build a more intentional declarative json config approach, as discussed
// here: https://github.com/sourcecred/sourcecred/issues/1232#issuecomment-519538494
// This method is untested; please take care when modifying it!
import dedent from "../util/dedent";
import type {Command} from "./command";
import * as Common from "./common";
import stringify from "json-stable-stringify";
import {
type Project,
projectToJSON,
createProject as defaultProject,
} from "../core/project";
import {type RepoId} from "../plugins/github/repoId";
import {specToProject} from "../plugins/github/specToProject";
import {type GithubToken} from "../plugins/github/token";
import * as NullUtil from "../util/null";
function usage(print: (string) => void): void {
print(
dedent`\
usage: sourcecred gen-project PROJECT_ID
[--github GITHUB_SPEC [...]]
[--discourse-url DISCOURSE_URL]
sourcecred gen-project --help
Generates a SourceCred project configuration based on the provided specs.
A PROJECT_ID must be provided, and will be the name of the project.
Zero or more github specs may be provided; each GitHub spec can be of the
form OWNER/NAME (as in 'torvalds/linux') for loading a single repository,
or @owner (as in '@torvalds') for loading all repositories owned by a given
account.
A discourse url and discourse username may be provided. If one is provided,
then both must be. The discourse url is a url to a valid Discourse server,
as in 'https://discourse.sourcecred.io', and the username must be a valid
user on that server, as in 'credbot'. The user in question should not have
any special or admin permissions, so that it won't encounter hidden
messages.
All of the GitHub specs, and the Discourse specification (if it exists)
will be combined into a single project. The serialized project
configuration will be printed to stdout.
Arguments:
PROJECT_ID
Locally unique identifier for the project.
--github GITHUB_SPEC
A specification (in form 'OWNER/NAME' or '@OWNER') of GitHub
repositories to load.
--discourse-url DISCOURSE_URL
The url of a Discourse server to load.
--help
Show this help message and exit, as 'sourcecred help scores'.
Environment Variables:
SOURCECRED_GITHUB_TOKEN
API token for GitHub. This should be a 40-character hex
string. Required if using GitHub specs.
To generate a token, create a "Personal access token" at
<https://github.com/settings/tokens>. When loading data for
public repositories, no special permissions are required.
For private repositories, the 'repo' scope is required.
`.trimRight()
);
}
function die(std, message) {
std.err("fatal: " + message);
std.err("fatal: run 'sourcecred help gen-project' for help");
return 1;
}
export const genProject: Command = async (args, std) => {
let projectId: string | null = null;
let discourseUrl: string | null = null;
const githubSpecs: string[] = [];
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--help": {
usage(std.out);
return 0;
}
case "--github": {
if (++i >= args.length)
return die(std, "'--github' given without value");
githubSpecs.push(args[i]);
break;
}
case "--discourse-url": {
if (discourseUrl != null)
return die(std, "'--discourse-url' given multiple times");
if (++i >= args.length)
return die(std, "'--discourse-url' given without value");
discourseUrl = args[i];
break;
}
default: {
if (projectId != null) return die(std, "multiple project IDs provided");
projectId = args[i];
break;
}
}
}
if (projectId == null) {
return die(std, "no project ID provided");
}
const githubToken = Common.githubToken();
const project: Project = await createProject({
projectId,
githubSpecs,
discourseUrl,
githubToken,
});
const projectJSON = projectToJSON(project);
console.log(stringify(projectJSON));
return 0;
};
export async function createProject(opts: {|
+projectId: string,
+githubSpecs: $ReadOnlyArray<string>,
+discourseUrl: string | null,
+githubToken: ?GithubToken,
|}): Promise<Project> {
const {projectId, githubSpecs, discourseUrl, githubToken} = opts;
let repoIds: RepoId[] = [];
let discourseServer = null;
if (discourseUrl) {
discourseServer = {serverUrl: discourseUrl};
}
if (githubSpecs.length && githubToken == null) {
throw new Error("Provided GitHub specs without GitHub token.");
}
for (const spec of githubSpecs) {
const subproject = await specToProject(spec, NullUtil.get(githubToken));
repoIds = repoIds.concat(subproject.repoIds);
}
return defaultProject({id: projectId, repoIds, discourseServer});
}
export default genProject;
export const help: Command = async (args, std) => {
if (args.length === 0) {
usage(std.out);
return 0;
} else {
usage(std.err);
return 1;
}
};

View File

@ -4,13 +4,6 @@
import type {Command} from "./command";
import dedent from "../util/dedent";
import {help as loadHelp} from "./load";
import {help as scoresHelp} from "./scores";
import {help as outputHelp} from "./output";
import {help as clearHelp} from "./clear";
import {help as genProjectHelp} from "./genProject";
import {help as discourseHelp} from "./discourse";
const help: Command = async (args, std) => {
if (args.length === 0) {
usage(std.out);
@ -19,12 +12,6 @@ const help: Command = async (args, std) => {
const command = args[0];
const subHelps: {[string]: Command} = {
help: metaHelp,
load: loadHelp,
scores: scoresHelp,
output: outputHelp,
clear: clearHelp,
"gen-project": genProjectHelp,
discourse: discourseHelp,
};
if (subHelps[command] !== undefined) {
return subHelps[command](args.slice(1), std);
@ -43,12 +30,6 @@ function usage(print: (string) => void): void {
sourcecred [--version] [--help]
Commands:
load load repository data into SourceCred
clear clear SoucrceCred data
scores print SourceCred scores to stdout
output print SourceCred data output to stdout
gen-project print a SourceCred project config to stdout
discourse load a Discourse server into SourceCred
help show this help message
Use 'sourcecred help COMMAND' for help about an individual command.

View File

@ -25,26 +25,6 @@ describe("cli/help", () => {
});
});
it("prints help about 'sourcecred load'", async () => {
expect(await run(help, ["load"])).toEqual({
exitCode: 0,
stdout: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred load/),
]),
stderr: [],
});
});
it("prints help about 'sourcecred clear'", async () => {
expect(await run(help, ["clear"])).toEqual({
exitCode: 0,
stdout: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred clear/),
]),
stderr: [],
});
});
it("fails when given an unknown command", async () => {
expect(await run(help, ["wat"])).toEqual({
exitCode: 1,

View File

@ -1,204 +0,0 @@
// @flow
// Implementation of `sourcecred load`
import dedent from "../util/dedent";
import {LoggingTaskReporter} from "../util/taskReporter";
import type {Command} from "./command";
import * as Common from "./common";
import * as Weights from "../core/weights";
import {projectFromJSON, type Project} from "../core/project";
import {load} from "../api/load";
import {specToProject} from "../plugins/github/specToProject";
import fs from "fs-extra";
import {type PluginDeclaration} from "../analysis/pluginDeclaration";
import {declaration as discourseDeclaration} from "../plugins/discourse/declaration";
import {declaration as githubDeclaration} from "../plugins/github/declaration";
import {declaration as identityDeclaration} from "../plugins/identity/declaration";
import {partialParams} from "../analysis/timeline/params";
function usage(print: (string) => void): void {
print(
dedent`\
usage: sourcecred load [PROJECT_SPEC...]
[--weights WEIGHTS_FILE]
[--project PROJECT_FILE]
sourcecred load --help
Load a target project, generating a cred attribution for it.
PROJET_SPEC is a string that describes a project.
Currently, it must be a GitHub repository in the form OWNER/NAME: for
example, torvalds/linux. Support for more PROJECT_SPECS will be added
shortly.
Arguments:
PROJECT_SPEC:
Identifier of a project to load.
--project PROJECT_FILE
Path to a json file which contains a project configuration.
That project will be loaded.
--weights WEIGHTS_FILE
Path to a json file which contains a weights configuration.
This will be used instead of the default weights and persisted.
--help
Show this help message and exit, as 'sourcecred help load'.
Environment variables:
SOURCECRED_GITHUB_TOKEN
API token for GitHub. This should be a 40-character hex
string. Required if using the GitHub plugin; ignored
otherwise.
To generate a token, create a "Personal access token" at
<https://github.com/settings/tokens>. When loading data for
public repositories, no special permissions are required.
For private repositories, the 'repo' scope is required.
SOURCECRED_INITIATIVES_DIRECTORY
Local path to a directory containing json files with
initiative declarations. Required when using the Initiatives
plugin; ignored otherwise.
SOURCECRED_DIRECTORY
Directory owned by SourceCred, in which data, caches,
registries, etc. are stored. Optional: defaults to a
directory 'sourcecred' under your OS's temporary directory;
namely:
${Common.defaultSourcecredDirectory()}
`.trimRight()
);
}
function die(std, message) {
std.err("fatal: " + message);
std.err("fatal: run 'sourcecred help load' for help");
return 1;
}
const loadCommand: Command = async (args, std) => {
const projectSpecs: string[] = [];
const projectPaths: string[] = [];
let weightsPath: ?string;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--help": {
usage(std.out);
return 0;
}
case "--weights": {
if (weightsPath != null)
return die(std, "'--weights' given multiple times");
if (++i >= args.length)
return die(std, "'--weights' given without value");
weightsPath = args[i];
break;
}
case "--project": {
if (++i >= args.length)
return die(std, "'--project' given without value");
projectPaths.push(args[i]);
break;
}
default: {
projectSpecs.push(args[i]);
break;
}
}
}
if (projectSpecs.length === 0 && projectPaths.length === 0) {
return die(std, "projects not specified");
}
let weights = Weights.empty();
if (weightsPath) {
weights = await loadWeightOverrides(weightsPath);
}
const initiativesDirectory = Common.initiativesDirectory();
const githubToken = Common.githubToken();
if (githubToken == null) {
return die(std, "SOURCECRED_GITHUB_TOKEN not set");
}
const taskReporter = new LoggingTaskReporter();
const specProjects: $ReadOnlyArray<Project> = await Promise.all(
projectSpecs.map((s) => specToProject(s, githubToken))
);
const manualProjects = await Promise.all(projectPaths.map(loadProject));
const projects = specProjects.concat(manualProjects);
const optionses = projects.map((project) => {
const plugins: PluginDeclaration[] = [];
if (project.discourseServer != null) {
plugins.push(discourseDeclaration);
}
if (project.repoIds.length) {
plugins.push(githubDeclaration);
}
if (project.identities.length) {
plugins.push(identityDeclaration);
}
const params = partialParams(project.timelineCredParams);
return {
project,
params,
weightsOverrides: weights,
plugins,
discordToken: Common.discordToken(),
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken,
initiativesDirectory,
};
});
// Deliberately load in serial because GitHub requests that their API not
// be called concurrently
for (const options of optionses) {
await load(options, taskReporter);
}
return 0;
};
const loadWeightOverrides = async (path: string) => {
if (!(await fs.exists(path))) {
throw new Error("Could not find the weights file");
}
const raw = await fs.readFile(path, "utf-8");
const weightsJSON = JSON.parse(raw);
try {
return Weights.fromJSON(weightsJSON);
} catch (e) {
throw new Error(`provided weights file is invalid:\n${e}`);
}
};
const loadProject = async (path: string) => {
if (!(await fs.exists(path))) {
throw new Error(`Project path ${path} does not exist`);
}
const raw = await fs.readFile(path, "utf-8");
const json = JSON.parse(raw);
try {
return projectFromJSON(json);
} catch (e) {
throw new Error(`project at path ${path} is invalid:\n${e}`);
}
};
export const help: Command = async (args, std) => {
if (args.length === 0) {
usage(std.out);
return 0;
} else {
usage(std.err);
return 1;
}
};
export default loadCommand;

View File

@ -1,228 +0,0 @@
// @flow
import tmp from "tmp";
import fs from "fs-extra";
import {LoggingTaskReporter} from "../util/taskReporter";
import {NodeAddress} from "../core/graph";
import {run} from "./testUtil";
import loadCommand, {help} from "./load";
import type {LoadOptions} from "../api/load";
import * as Weights from "../core/weights";
import * as Common from "./common";
import {declaration as githubDeclaration} from "../plugins/github/declaration";
import {createProject} from "../core/project";
import {makeRepoId, stringToRepoId} from "../plugins/github/repoId";
import {validateToken} from "../plugins/github/token";
import {defaultParams} from "../analysis/timeline/params";
jest.mock("../api/load", () => ({load: jest.fn()}));
type JestMockFn = $Call<typeof jest.fn>;
const load: JestMockFn = (require("../api/load").load: any);
describe("cli/load", () => {
const exampleGithubToken = validateToken("0".repeat(40));
const exampleDiscordToken = "fakeBotToken";
beforeEach(() => {
jest.clearAllMocks();
// Tests should call `newSourcecredDirectory` directly when they
// need the value. We call it here in case a test needs it to be set
// but does not care about the particular value.
newSourcecredDirectory();
});
function newSourcecredDirectory() {
const dirname = tmp.dirSync().name;
process.env.SOURCECRED_DIRECTORY = dirname;
process.env.SOURCECRED_GITHUB_TOKEN = exampleGithubToken;
process.env.SOURCECRED_DISCORD_TOKEN = exampleDiscordToken;
process.env.SOURCECRED_INITIATIVES_DIRECTORY = tmp.dirSync().name;
return dirname;
}
describe("'help' command", () => {
it("prints usage when given no arguments", async () => {
expect(await run(help, [])).toEqual({
exitCode: 0,
stdout: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred load/),
]),
stderr: [],
});
});
it("fails when given arguments", async () => {
expect(await run(help, ["foo/bar"])).toEqual({
exitCode: 1,
stdout: [],
stderr: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred load/),
]),
});
});
});
describe("'load' command wrapper", () => {
it("prints usage with '--help'", async () => {
expect(await run(loadCommand, ["--help"])).toEqual({
exitCode: 0,
stdout: expect.arrayContaining([
expect.stringMatching(/^usage: sourcecred load/),
]),
stderr: [],
});
});
it("calls load with a single repo", async () => {
const invocation = run(loadCommand, ["foo/bar"]);
const expectedOptions: LoadOptions = {
project: createProject({
id: "foo/bar",
repoIds: [makeRepoId("foo", "bar")],
}),
params: defaultParams(),
weightsOverrides: Weights.empty(),
plugins: [githubDeclaration],
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: exampleGithubToken,
discordToken: exampleDiscordToken,
initiativesDirectory: Common.initiativesDirectory(),
};
expect(await invocation).toEqual({
exitCode: 0,
stdout: [],
stderr: [],
});
expect(load).toHaveBeenCalledWith(
expectedOptions,
expect.any(LoggingTaskReporter)
);
});
it("calls load with multiple repos", async () => {
const invocation = run(loadCommand, ["foo/bar", "zoink/zod"]);
const expectedOptions: (string) => LoadOptions = (projectId: string) => ({
project: createProject({
id: projectId,
repoIds: [stringToRepoId(projectId)],
}),
params: defaultParams(),
weightsOverrides: Weights.empty(),
plugins: [githubDeclaration],
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: exampleGithubToken,
discordToken: exampleDiscordToken,
initiativesDirectory: Common.initiativesDirectory(),
});
expect(await invocation).toEqual({
exitCode: 0,
stdout: [],
stderr: [],
});
expect(load).toHaveBeenCalledWith(
expectedOptions("foo/bar"),
expect.any(LoggingTaskReporter)
);
expect(load).toHaveBeenCalledWith(
expectedOptions("zoink/zod"),
expect.any(LoggingTaskReporter)
);
});
it("loads the weights, if provided", async () => {
const weights = Weights.empty();
weights.nodeWeights.set(NodeAddress.empty, 33);
const weightsJSON = Weights.toJSON(weights);
const weightsFile = tmp.tmpNameSync();
fs.writeFileSync(weightsFile, JSON.stringify(weightsJSON));
const invocation = run(loadCommand, [
"foo/bar",
"--weights",
weightsFile,
]);
const expectedOptions: LoadOptions = {
project: createProject({
id: "foo/bar",
repoIds: [makeRepoId("foo", "bar")],
}),
params: defaultParams(),
weightsOverrides: weights,
plugins: [githubDeclaration],
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: exampleGithubToken,
discordToken: exampleDiscordToken,
initiativesDirectory: Common.initiativesDirectory(),
};
expect(await invocation).toEqual({
exitCode: 0,
stdout: [],
stderr: [],
});
expect(load).toHaveBeenCalledWith(
expectedOptions,
expect.any(LoggingTaskReporter)
);
});
describe("errors if", () => {
async function expectFailure({args, message}) {
expect(await run(loadCommand, args)).toEqual({
exitCode: 1,
stdout: [],
stderr: message,
});
expect(load).not.toHaveBeenCalled();
}
it("no projects specified", async () => {
await expectFailure({
args: [],
message: [
"fatal: projects not specified",
"fatal: run 'sourcecred help load' for help",
],
});
});
it("the weights file does not exist", async () => {
const weightsFile = tmp.tmpNameSync();
await expectFailure({
args: ["foo/bar", "--weights", weightsFile],
message: [
expect.stringMatching("^Error: Could not find the weights file"),
],
});
});
it("the weights file is invalid", async () => {
const weightsFile = tmp.tmpNameSync();
fs.writeFileSync(weightsFile, JSON.stringify({weights: 3}));
await expectFailure({
args: ["foo/bar", "--weights", weightsFile],
message: [
expect.stringMatching("^Error: provided weights file is invalid"),
],
});
});
it("the repo identifier is invalid", async () => {
await expectFailure({
args: ["missing_delimiter"],
message: [
expect.stringMatching("^Error: invalid spec: missing_delimiter"),
],
});
});
it("the SOURCECRED_GITHUB_TOKEN is unset", async () => {
delete process.env.SOURCECRED_GITHUB_TOKEN;
await expectFailure({
args: ["missing_delimiter"],
message: [
"fatal: SOURCECRED_GITHUB_TOKEN not set",
"fatal: run 'sourcecred help load' for help",
],
});
});
});
});
});

View File

@ -1,23 +0,0 @@
// @flow
import {handlingErrors} from "./command";
import sourcecred from "./sourcecred";
require("../tools/entry");
export default function main(): Promise<void> {
return handlingErrors(sourcecred)(process.argv.slice(2), {
out: (x) => console.log(x),
err: (x) => console.error(x),
}).then((exitCode) => {
process.exitCode = exitCode;
});
}
// Only run in the Webpack bundle, not as a Node module (during tests).
/* istanbul ignore next */
/*:: declare var __webpack_require__: mixed; */
// eslint-disable-next-line camelcase
if (typeof __webpack_require__ !== "undefined") {
main();
}

View File

@ -1,110 +0,0 @@
// @flow
// Implementation of `sourcecred output`.
import {toCompat} from "../util/compat";
import {fromJSON as pluginsFromJSON} from "../analysis/pluginDeclaration";
import {
fromTimelineCredAndPlugins,
COMPAT_INFO as OUTPUT_COMPAT_INFO,
} from "../analysis/output";
import path from "path";
import fs from "fs-extra";
import dedent from "../util/dedent";
import type {Command} from "./command";
import * as Common from "./common";
import stringify from "json-stable-stringify";
import {TimelineCred} from "../analysis/timeline/timelineCred";
import {directoryForProjectId} from "../core/project_io";
function usage(print: (string) => void): void {
print(
dedent`\
usage: sourcecred output PROJECT_ID [--help]
Print the SourceCred data output for a given PROJECT_ID.
Data must already be loaded for the given PROJECT_ID, using
'sourcecred load PROJECT_ID'
PROJECT_ID refers to a project, as loaded by the \`load\` command.
Run \`sourcecred load --help\` for details.
Arguments:
PROJECT_ID
Already-loaded project for which to load data.
--help
Show this help message and exit, as 'sourcecred help output'.
Environment Variables:
SOURCECRED_DIRECTORY
Directory owned by SourceCred, in which data, caches,
registries, etc. are stored. Optional: defaults to a
directory 'sourcecred' under your OS's temporary directory;
namely:
${Common.defaultSourcecredDirectory()}
`.trimRight()
);
}
function die(std, message) {
std.err("fatal: " + message);
std.err("fatal: run 'sourcecred help output' for help");
return 1;
}
export const output: Command = async (args, std) => {
let projectId: string | null = null;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--help": {
usage(std.out);
return 0;
}
default: {
if (projectId != null) return die(std, "multiple project IDs provided");
projectId = args[i];
break;
}
}
}
if (projectId == null) {
return die(std, "no project ID provided");
}
const projectDirectory = directoryForProjectId(
projectId,
Common.sourcecredDirectory()
);
const credFile = path.join(projectDirectory, "cred.json");
const pluginsFile = path.join(projectDirectory, "pluginDeclarations.json");
if (!fs.existsSync(credFile) || !fs.existsSync(pluginsFile)) {
std.err(`fatal: project ${projectId} not loaded`);
std.err(`Try running \`sourcecred load ${projectId}\` first.`);
return 1;
}
const credBlob = await fs.readFile(credFile);
const credJSON = JSON.parse(credBlob.toString());
const timelineCred = TimelineCred.fromJSON(credJSON);
const pluginsBlob = await fs.readFile(pluginsFile);
const pluginsJSON = JSON.parse(pluginsBlob.toString());
const plugins = pluginsFromJSON(pluginsJSON);
const output = fromTimelineCredAndPlugins(timelineCred, plugins);
const compatOutput = toCompat(OUTPUT_COMPAT_INFO, output);
std.out(stringify(compatOutput, {space: 2}));
return 0;
};
export default output;
export const help: Command = async (args, std) => {
if (args.length === 0) {
usage(std.out);
return 0;
} else {
usage(std.err);
return 1;
}
};

View File

@ -1,134 +0,0 @@
// @flow
// Implementation of `sourcecred scores`.
import {toCompat, type Compatible} from "../util/compat";
import path from "path";
import fs from "fs-extra";
import dedent from "../util/dedent";
import type {Command} from "./command";
import * as Common from "./common";
import stringify from "json-stable-stringify";
import {
TimelineCred,
type Interval,
type CredNode,
} from "../analysis/timeline/timelineCred";
import {directoryForProjectId} from "../core/project_io";
import {NodeAddress} from "../core/graph";
const COMPAT_INFO = {type: "sourcecred/cli/scores", version: "0.2.0"};
function usage(print: (string) => void): void {
print(
dedent`\
usage: sourcecred scores PROJECT_ID [--help]
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'
PROJECT_ID refers to a project, as loaded by the \`load\` command.
Run \`sourcecred load --help\` for details.
Arguments:
PROJECT_ID
Already-loaded project for which to load data.
--help
Show this help message and exit, as 'sourcecred help scores'.
Environment Variables:
SOURCECRED_DIRECTORY
Directory owned by SourceCred, in which data, caches,
registries, etc. are stored. Optional: defaults to a
directory 'sourcecred' under your OS's temporary directory;
namely:
${Common.defaultSourcecredDirectory()}
`.trimRight()
);
}
function die(std, message) {
std.err("fatal: " + message);
std.err("fatal: run 'sourcecred help scores' for help");
return 1;
}
export type NodeOutput = {|
// The components of the SourceCred address for the node
// Conventionally, the first two components designate what plugin
// generated the node, as in [PLUGIN_AUTHOR, PLUGIN_NAME, ...]
// Subsequent components are created according to plugin-specific logic.
+address: $ReadOnlyArray<string>,
+totalCred: number,
+intervalCred: $ReadOnlyArray<number>,
|};
export type ScoreOutput = Compatible<{|
+users: $ReadOnlyArray<NodeOutput>,
+intervals: $ReadOnlyArray<Interval>,
|}>;
export const scores: Command = async (args, std) => {
let projectId: string | null = null;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--help": {
usage(std.out);
return 0;
}
default: {
if (projectId != null) return die(std, "multiple project IDs provided");
projectId = args[i];
break;
}
}
}
if (projectId == null) {
return die(std, "no project ID provided");
}
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 credBlob = await fs.readFile(credFile);
const credJSON = JSON.parse(credBlob.toString());
const timelineCred = TimelineCred.fromJSON(credJSON);
const userOutput: NodeOutput[] = timelineCred
.userNodes()
.map((n: CredNode) => {
const address = NodeAddress.toParts(n.node.address);
return {
address,
intervalCred: n.cred,
totalCred: n.total,
};
});
const output: ScoreOutput = toCompat(COMPAT_INFO, {
users: userOutput,
intervals: timelineCred.intervals(),
});
std.out(stringify(output, {space: 2}));
return 0;
};
export default scores;
export const help: Command = async (args, std) => {
if (args.length === 0) {
usage(std.out);
return 0;
} else {
usage(std.err);
return 1;
}
};

View File

@ -1,50 +0,0 @@
// @flow
// Implementation of the root `sourcecred` command.
import type {Command} from "./command";
import {VERSION_SHORT} from "../core/version";
import help from "./help";
import load from "./load";
import scores from "./scores";
import output from "./output";
import clear from "./clear";
import genProject from "./genProject";
import discourse from "./discourse";
import discord from "./discord";
const sourcecred: Command = async (args, std) => {
if (args.length === 0) {
help([], {out: std.err, err: std.err});
return 1;
}
switch (args[0]) {
case "--version":
std.out("sourcecred " + VERSION_SHORT);
return 0;
case "--help":
case "help":
return help(args.slice(1), std);
case "load":
return load(args.slice(1), std);
case "clear":
return clear(args.slice(1), std);
case "scores":
return scores(args.slice(1), std);
case "output":
return output(args.slice(1), std);
case "gen-project":
return genProject(args.slice(1), std);
case "discord":
return discord(args.slice(1), std);
case "discourse":
return discourse(args.slice(1), std);
default:
std.err("fatal: unknown command: " + JSON.stringify(args[0]));
std.err("fatal: run 'sourcecred help' for commands and usage");
return 1;
}
};
export default sourcecred;

View File

@ -1,77 +0,0 @@
// @flow
import {run} from "./testUtil";
import sourcecred from "./sourcecred";
function mockCommand(name) {
return jest.fn().mockImplementation(async (args, std) => {
std.out(`out(${name}): ${JSON.stringify(args)}`);
std.err(`err(${name})`);
return args.length;
});
}
jest.mock("./help", () => mockCommand("help"));
jest.mock("./load", () => mockCommand("load"));
jest.mock("./clear", () => mockCommand("clear"));
describe("cli/sourcecred", () => {
it("fails with usage when invoked with no arguments", async () => {
expect(await run(sourcecred, [])).toEqual({
exitCode: 1,
stdout: [],
stderr: ["out(help): []", "err(help)"],
});
});
it("responds to '--version'", async () => {
expect(await run(sourcecred, ["--version"])).toEqual({
exitCode: 0,
stdout: [expect.stringMatching(/^sourcecred v\d+\.\d+\.\d+$/)],
stderr: [],
});
});
it("responds to '--help'", async () => {
expect(await run(sourcecred, ["--help"])).toEqual({
exitCode: 0,
stdout: ["out(help): []"],
stderr: ["err(help)"],
});
});
it("responds to 'help'", async () => {
expect(await run(sourcecred, ["help"])).toEqual({
exitCode: 0,
stdout: ["out(help): []"],
stderr: ["err(help)"],
});
});
it("responds to 'load'", async () => {
expect(await run(sourcecred, ["load", "foo/bar", "foo/baz"])).toEqual({
exitCode: 2,
stdout: ['out(load): ["foo/bar","foo/baz"]'],
stderr: ["err(load)"],
});
});
it("responds to 'clear --all'", async () => {
expect(await run(sourcecred, ["clear", "--all"])).toEqual({
exitCode: 1,
stdout: ['out(clear): ["--all"]'],
stderr: ["err(clear)"],
});
});
it("fails given an unknown command", async () => {
expect(await run(sourcecred, ["wat"])).toEqual({
exitCode: 1,
stdout: [],
stderr: [
'fatal: unknown command: "wat"',
"fatal: run 'sourcecred help' for commands and usage",
],
});
});
});

View File

@ -11,7 +11,7 @@ jest.spyOn(console, "error").mockImplementation(() => {});
const logMock: JestMockFn<any, void> = console.log;
const errorMock: JestMockFn<any, void> = console.error;
describe("cli/main", () => {
describe("cli2/main", () => {
beforeEach(() => {
sourcecredMock.mockReset();
logMock.mockClear();