From 54ece536d3dc7681a9697291c73dc0c67cfa3ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Fri, 20 Sep 2019 12:08:27 +0200 Subject: [PATCH] Integrate the identity plugin (#1385) This commit integrates the identity plugin, which was created in #1384. It does this by adding explicit identity fields to the project configuration, which are then applied when loading the graph in `api/load.js`. The actual integration is quite straightforward. Test plan: The underlying logic is thoroughly tested; I added one new test case to verify that it is integrated properly. Since the project compat has changed, I've updated all the snapshots. Prior to merging this PR, I will produce one "integration test", using this code to do identity resolution for a real project (i.e. on the SourceCred instance itself). --- CHANGELOG.md | 1 + .../project.json | 2 +- src/api/load.js | 12 ++++++++++- src/api/load.test.js | 21 +++++++++++++++++++ src/cli/discourse.js | 1 + src/cli/genProject.js | 2 +- src/cli/load.js | 4 ++++ src/cli/load.test.js | 3 +++ src/core/project.js | 4 +++- src/core/project.test.js | 7 +++++++ src/core/project_io.test.js | 2 ++ src/plugins/github/specToProject.js | 2 ++ src/plugins/github/specToProject.test.js | 8 ++++++- 13 files changed, 64 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c60e61..8af7eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] +- Enable combining different user identities together (#1385) - Add `sourcecred discourse` for loading Discourse servers (#1374) - Breaking: Change output format for the scores command (#1372) - Include top nodes for every type in Timeline Cred (#1358) diff --git a/sharness/__snapshots__/example-github-load/projects/c291cmNlY3JlZC10ZXN0L2V4YW1wbGUtZ2l0aHVi/project.json b/sharness/__snapshots__/example-github-load/projects/c291cmNlY3JlZC10ZXN0L2V4YW1wbGUtZ2l0aHVi/project.json index 7e31e86..e590180 100644 --- a/sharness/__snapshots__/example-github-load/projects/c291cmNlY3JlZC10ZXN0L2V4YW1wbGUtZ2l0aHVi/project.json +++ b/sharness/__snapshots__/example-github-load/projects/c291cmNlY3JlZC10ZXN0L2V4YW1wbGUtZ2l0aHVi/project.json @@ -1 +1 @@ -[{"type":"sourcecred/project","version":"0.2.0"},{"discourseServer":null,"id":"sourcecred-test/example-github","repoIds":[{"name":"example-github","owner":"sourcecred-test"}]}] \ No newline at end of file +[{"type":"sourcecred/project","version":"0.3.0"},{"discourseServer":null,"id":"sourcecred-test/example-github","identities":[],"repoIds":[{"name":"example-github","owner":"sourcecred-test"}]}] \ No newline at end of file diff --git a/src/api/load.js b/src/api/load.js index f36d4f9..402a98b 100644 --- a/src/api/load.js +++ b/src/api/load.js @@ -15,6 +15,7 @@ import {setupProjectDirectory} from "../core/project_io"; import {loadDiscourse} from "../plugins/discourse/loadDiscourse"; import {type PluginDeclaration} from "../analysis/pluginDeclaration"; import * as NullUtil from "../util/null"; +import {nodeContractions} from "../plugins/identity/nodeContractions"; export type LoadOptions = {| +project: Project, @@ -91,7 +92,16 @@ export async function load( ]); const pluginGraphs = await Promise.all(pluginGraphPromises); - const graph = Graph.merge(pluginGraphs); + let graph = Graph.merge(pluginGraphs); + const {identities, discourseServer} = project; + if (identities.length) { + const serverUrl = + discourseServer == null ? null : discourseServer.serverUrl; + const contractions = nodeContractions(identities, serverUrl); + // Only apply contractions if identities have been specified, since it involves + // a full Graph copy + graph = graph.contractNodes(contractions); + } const projectDirectory = await setupProjectDirectory( project, diff --git a/src/api/load.test.js b/src/api/load.test.js index 66dfb14..3b41167 100644 --- a/src/api/load.test.js +++ b/src/api/load.test.js @@ -7,6 +7,7 @@ import fs from "fs-extra"; import type {Options as LoadGraphOptions} from "../plugins/github/loadGraph"; import type {Options as LoadDiscourseOptions} from "../plugins/discourse/loadDiscourse"; +import {nodeContractions} from "../plugins/identity/nodeContractions"; import type {Project} from "../core/project"; import { directoryForProjectId, @@ -66,6 +67,7 @@ describe("api/load", () => { serverUrl: discourseServerUrl, apiUsername: discourseApiUsername, }, + identities: [], }; deepFreeze(project); const githubToken = "EXAMPLE_TOKEN"; @@ -222,4 +224,23 @@ describe("api/load", () => { const expectedJSON = discourseGraph().toJSON(); expect(graphJSON).toEqual(expectedJSON); }); + + it("applies identity transformations, if present in the project", async () => { + const {options, taskReporter, sourcecredDirectory} = example(); + const identity = {username: "identity", aliases: []}; + const newProject = {...options.project, identities: [identity]}; + const newOptions = {...options, project: newProject}; + await load(newOptions, taskReporter); + const projectDirectory = directoryForProjectId( + project.id, + sourcecredDirectory + ); + const graphFile = path.join(projectDirectory, "graph.json"); + const graphJSON = JSON.parse(await fs.readFile(graphFile)); + const identityGraph = combinedGraph().contractNodes( + nodeContractions([identity], discourseServerUrl) + ); + const expectedJSON = identityGraph.toJSON(); + expect(graphJSON).toEqual(expectedJSON); + }); }); diff --git a/src/cli/discourse.js b/src/cli/discourse.js index 4c3687c..64eede3 100644 --- a/src/cli/discourse.js +++ b/src/cli/discourse.js @@ -105,6 +105,7 @@ const command: Command = async (args, std) => { id: projectId, repoIds: [], discourseServer: {serverUrl, apiUsername}, + identities: [], }; const taskReporter = new LoggingTaskReporter(); let weights = defaultWeights(); diff --git a/src/cli/genProject.js b/src/cli/genProject.js index 6736bf6..342c9cd 100644 --- a/src/cli/genProject.js +++ b/src/cli/genProject.js @@ -171,7 +171,7 @@ export async function createProject(opts: {| const subproject = await specToProject(spec, NullUtil.get(githubToken)); repoIds = repoIds.concat(subproject.repoIds); } - return {id: projectId, repoIds, discourseServer}; + return {id: projectId, repoIds, discourseServer, identities: []}; } export default genProject; diff --git a/src/cli/load.js b/src/cli/load.js index ccf6433..39b9380 100644 --- a/src/cli/load.js +++ b/src/cli/load.js @@ -14,6 +14,7 @@ import {partialParams} from "../analysis/timeline/params"; 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"; function usage(print: (string) => void): void { print( @@ -132,6 +133,9 @@ const loadCommand: Command = async (args, std) => { if (project.repoIds.length) { plugins.push(githubDeclaration); } + if (project.identities.length) { + plugins.push(identityDeclaration); + } return { project, params, diff --git a/src/cli/load.test.js b/src/cli/load.test.js index f0d3bb9..c484633 100644 --- a/src/cli/load.test.js +++ b/src/cli/load.test.js @@ -77,6 +77,7 @@ describe("cli/load", () => { id: "foo/bar", repoIds: [makeRepoId("foo", "bar")], discourseServer: null, + identities: [], }, params: defaultParams(), plugins: [githubDeclaration], @@ -102,6 +103,7 @@ describe("cli/load", () => { id: projectId, repoIds: [stringToRepoId(projectId)], discourseServer: null, + identities: [], }, params: defaultParams(), plugins: [githubDeclaration], @@ -140,6 +142,7 @@ describe("cli/load", () => { id: "foo/bar", repoIds: [makeRepoId("foo", "bar")], discourseServer: null, + identities: [], }, params: partialParams({weights}), plugins: [githubDeclaration], diff --git a/src/core/project.js b/src/core/project.js index cac79ca..c96d517 100644 --- a/src/core/project.js +++ b/src/core/project.js @@ -3,6 +3,7 @@ import base64url from "base64url"; import {type RepoId} from "../core/repoId"; import {toCompat, fromCompat, type Compatible} from "../util/compat"; +import {type Identity} from "../plugins/identity/identity"; export type ProjectId = string; @@ -29,9 +30,10 @@ export type Project = {| +serverUrl: string, +apiUsername: string, |} | null, + +identities: $ReadOnlyArray, |}; -const COMPAT_INFO = {type: "sourcecred/project", version: "0.2.0"}; +const COMPAT_INFO = {type: "sourcecred/project", version: "0.3.0"}; export type ProjectJSON = Compatible; diff --git a/src/core/project.test.js b/src/core/project.test.js index 7605019..e35d7d0 100644 --- a/src/core/project.test.js +++ b/src/core/project.test.js @@ -18,11 +18,18 @@ describe("core/project", () => { id: "foo/bar", repoIds: [foobar], discourseServer: null, + identities: [], }); const p2: Project = deepFreeze({ id: "@foo", repoIds: [foobar, foozod], discourseServer: {serverUrl: "https://example.com", apiUsername: "credbot"}, + identities: [ + { + username: "example", + aliases: ["github/example"], + }, + ], }); describe("to/fro JSON", () => { it("round trip is identity", () => { diff --git a/src/core/project_io.test.js b/src/core/project_io.test.js index 8681014..7076a78 100644 --- a/src/core/project_io.test.js +++ b/src/core/project_io.test.js @@ -23,11 +23,13 @@ describe("core/project_io", () => { id: "foo/bar", repoIds: [foobar], discourseServer: null, + identities: [], }); const p2: Project = deepFreeze({ id: "@foo", repoIds: [foobar, foozod], discourseServer: {serverUrl: "https://example.com", apiUsername: "credbot"}, + identities: [{username: "foo", aliases: ["github/foo", "discourse/foo"]}], }); it("setupProjectDirectory results in a loadable project", async () => { diff --git a/src/plugins/github/specToProject.js b/src/plugins/github/specToProject.js index 5c5f2e2..8acc293 100644 --- a/src/plugins/github/specToProject.js +++ b/src/plugins/github/specToProject.js @@ -37,6 +37,7 @@ export async function specToProject( id: spec, repoIds: [stringToRepoId(spec)], discourseServer: null, + identities: [], }; return project; } else if (spec.match(ownerSpecMatcher)) { @@ -46,6 +47,7 @@ export async function specToProject( id: spec, repoIds: org.repos, discourseServer: null, + identities: [], }; return project; } diff --git a/src/plugins/github/specToProject.test.js b/src/plugins/github/specToProject.test.js index 4a17231..5023375 100644 --- a/src/plugins/github/specToProject.test.js +++ b/src/plugins/github/specToProject.test.js @@ -18,6 +18,7 @@ describe("plugins/github/specToProject", () => { id: spec, repoIds: [stringToRepoId(spec)], discourseServer: null, + identities: [], }; const actual = await specToProject(spec, "FAKE_TOKEN"); expect(expected).toEqual(actual); @@ -31,7 +32,12 @@ describe("plugins/github/specToProject", () => { fetchGithubOrg.mockResolvedValueOnce(fakeOrg); const actual = await specToProject(spec, token); expect(fetchGithubOrg).toHaveBeenCalledWith(fakeOrg.name, token); - const expected: Project = {id: spec, repoIds: repos, discourseServer: null}; + const expected: Project = { + id: spec, + repoIds: repos, + discourseServer: null, + identities: [], + }; expect(actual).toEqual(expected); }); describe("fails for malformed spec strings", () => {