add `sourcecred/scores` (#1223)

The scores are lightly processed from their internal representation.
Example usage:

```
$ yarn backend;
$ node bin/sourcecred.js load sourcecred/sourcecred
$ node bin/sourcecred.js scores sourcecred/sourcecred > scores.json
```

The data structure is as follows:

```js
export type NodeOutput = {|
  +id: string,
  +totalCred: number,
  +intervalCred: $ReadOnlyArray<number>,
|};

export type ScoreOutput = Compatible<{|
  +users: $ReadOnlyArray<NodeOutput>,
  +intervals: $ReadOnlyArray<Interval>,
|}>;
```

Test plan: I added sharness tests at `sharness/test_cli_scores.t`.
In the past, we've used javascript tests for CLI commands. However,
those are pretty time-consuming to write, and are less robust than
simply running the command from bash. Check the snapshot for a sense of
what the new data format looks like. Also, the snapshot updater now
updates this snapshot too.

Relevant for #1047.
Thanks to @Beanow for feedback on the output format and design.
Thanks to @wchargin for help in code review.
This commit is contained in:
Dandelion Mané 2019-07-14 17:05:13 +01:00 committed by GitHub
parent 8e0bbcf597
commit 88f736d180
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 559 additions and 0 deletions

View File

@ -25,6 +25,9 @@ node "${SOURCECRED_BIN}/generateGithubGraphqlFlowTypes.js" \
echo "Updating sharness/test_load_example_github.t"
(cd sharness; UPDATE_SNAPSHOT=1 ./test_load_example_github.t -l)
echo "Updating sharness/test_cli_scores.t"
(cd sharness; UPDATE_SNAPSHOT=1 ./test_cli_scores.t -l)
echo "Updating git/loadRepositoryTest.sh"
./src/plugins/git/loadRepositoryTest.sh -u --no-build

View File

@ -0,0 +1,312 @@
[
{
"type": "sourcecred/cli/scores",
"version": "0.1.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": [
{
"id": "decentralion",
"intervalCred": [
9.383869639663432,
4.691404861944296,
5.407033783069464,
4.710039491158759,
2.3542990795642016,
1.1764257521110257,
0.5874968415385614,
0.2930472936432958,
0.14585208714512637,
3.367558953101356,
1.6854511612955203,
0.8443081404654077,
0.4235721757793936,
0.21292350141151434,
0.10718062099935051,
0.024900766153114956,
0.01234421271106403,
0.5639545904301708,
0.2826874903289227,
0.14116839002688855,
0.07018698540293254,
0.03469827124411613,
0.017043428850844578,
0.008340605522915224,
0.004101657419219121,
0.0020470494713788713,
0.2330964724510903,
0.11911297405802071,
1.9437472461067256,
1.2458984494463696,
0.6241050193500056,
0.3126272039780528,
0.1565536991433818,
0.07836083092825474,
0.039204376624594785,
0.019603716512520068,
0.009792000539440526,
0.004877883689977619,
0.002415531283206432,
0.0011833764411626655,
0.0005707084414700211,
0.00027081941634511306,
0.00012783639288752084,
0.00006135289874258643,
0.0000305382491069489,
0.000015865125198178594,
0.00000853390543554515,
8.18684979546365e-7
],
"totalCred": 41.343602084119254
},
{
"id": "wchargin",
"intervalCred": [
3.6161303603365673,
1.808595138055704,
0.8429662169305362,
0.41496050884124025,
0.2082009204357985,
0.10482424788897443,
0.05312815846143868,
0.02726520635670421,
0.014304162854873638,
2.2125191718986437,
1.10458790120448,
0.5507113907845924,
0.2739375898456065,
0.13583138140098566,
0.0671968204068995,
0.5622879545500101,
0.2812501476404985,
0.5828425897456103,
0.2907110997589679,
0.14553090501705676,
0.07316266211904013,
0.03697655251687019,
0.018793983029648593,
0.00957810041733136,
0.004857695550904171,
0.0024326270136827742,
0.2691433657914406,
0.1320069450632447,
4.1818127134539065,
1.8168815303339467,
0.9072849705401523,
0.45306779096702615,
0.22629379832915772,
0.11306291780801501,
0.0565074977435401,
0.02825222067154737,
0.014135968052593194,
0.00708610060603924,
0.0035664608648019987,
0.00180761963284155,
0.0009247895955320864,
0.00047692960215594074,
0.0002460381163630061,
0.000125584355882677,
0.00006293037820568283,
0.00003086918845813727,
0.00001483325139261278,
0.000010864893434532599
],
"totalCred": 21.65638623230234
}
]
}
]

96
sharness/test_cli_scores.t Executable file
View File

@ -0,0 +1,96 @@
#!/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 repository 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
'
test_expect_success SETUP "--help should print usage info" '
run scores --help &&
grep -q "usage: sourcecred scores REPO_ID" out
'
test_expect_success SETUP "should fail for multiple repos" '
test_must_fail run_without_validation scores sourcecred/sourcecred torvalds/linux &&
grep -q "fatal: multiple repository IDs provided" err
'
test_expect_success SETUP "should fail for unloaded repo" '
test_must_fail run_without_validation scores torvalds/linux &&
grep -q "fatal: repository ID 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/example-github &&
mv out "${snapshot_file}"
'
test_expect_success SETUP "should be identical to the snapshot" '
run scores sourcecred/example-github &&
diff -u out ${snapshot_file}
'
test_done
# vim: ft=sh

View File

@ -7,6 +7,7 @@ import dedent from "../util/dedent";
import {help as loadHelp} from "./load";
import {help as analyzeHelp} from "./analyze";
import {help as pagerankHelp} from "./pagerank";
import {help as scoresHelp} from "./scores";
import {help as clearHelp} from "./clear";
const help: Command = async (args, std) => {
@ -19,6 +20,7 @@ const help: Command = async (args, std) => {
help: metaHelp,
load: loadHelp,
analyze: analyzeHelp,
scores: scoresHelp,
pagerank: pagerankHelp,
clear: clearHelp,
};

143
src/cli/scores.js Normal file
View File

@ -0,0 +1,143 @@
// @flow
// Implementation of `sourcecred scores`.
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";
import stringify from "json-stable-stringify";
import {
TimelineCred,
type Interval,
type CredNode,
} from "../analysis/timeline/timelineCred";
import {DEFAULT_CRED_CONFIG} from "../plugins/defaultCredConfig";
import {userNodeType} from "../plugins/github/declaration";
import * as GN from "../plugins/github/nodes";
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]
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'
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.
Arguments:
REPO_ID
Already-loaded repository 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 = {|
+id: string,
+totalCred: number,
+intervalCred: $ReadOnlyArray<number>,
|};
export type ScoreOutput = Compatible<{|
+users: $ReadOnlyArray<NodeOutput>,
+intervals: $ReadOnlyArray<Interval>,
|}>;
export const scores: Command = async (args, std) => {
let repoId: RepoId | null = null;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--help": {
usage(std.out);
return 0;
}
default: {
if (repoId != null) return die(std, "multiple repository IDs provided");
// Should be a repository.
repoId = stringToRepoId(args[i]);
break;
}
}
}
if (repoId == null) {
return die(std, "no repository 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.`);
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);
const userOutput: NodeOutput[] = timelineCred
.credSortedNodes(userNodeType.prefix)
.map((n: CredNode) => {
const address = n.node.address;
const structuredAddress = GN.fromRaw((address: any));
if (structuredAddress.type !== GN.USERLIKE_TYPE) {
throw new Error("invariant violation");
}
return {
id: structuredAddress.login,
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

@ -9,6 +9,7 @@ import help from "./help";
import load from "./load";
import analyze from "./analyze";
import pagerank from "./pagerank";
import scores from "./scores";
import clear from "./clear";
const sourcecred: Command = async (args, std) => {
@ -31,6 +32,8 @@ const sourcecred: Command = async (args, std) => {
return clear(args.slice(1), std);
case "pagerank":
return pagerank(args.slice(1), std);
case "scores":
return scores(args.slice(1), std);
default:
std.err("fatal: unknown command: " + JSON.stringify(args[0]));
std.err("fatal: run 'sourcecred help' for commands and usage");