Add CLI command to clear sourcecred data directory (#1111)
Resolves #1067 Adds the CLI commands: `sourcecred clear --all` -- removes the $SOURCECRED_DIRECTORY `sourcecred clear --cache` -- removes the cache directory `sourcecred clear --help` -- provides usage info `sourcecred clear` -- prompts the user to be more specific Test plan: The unit tests ensure that the command is properly wired into the sourcecred CLI, including help text integration. However, just to be safe, we can start by verifying that calling `sourcecred` without arguments lists the `clear` command as a valid option, and that calling `sourcecred help clear` prints help information. (Note: it's necessary to run `yarn backend` before testing these changes) The unit tests also ensure that the command removes the proper directories, so there isn't really a need to manually test it, although the reviewer may choose to do so to be safe. Although out of scope for unit tests on this function, we can also do integration tests, to make sure that running the clear command doesn't leave the sourcecred directory in an invalid state from the perspective of the `load` command. ```js $ yarn backend; $ node bin/sourcecred.js load sourcecred/example-github; $ node bin/sourcecred.js clear --cache; $ node bin/sourcecred.js load sourcecred/example-github; $ node bin/sourcecred.js clear --all; $ node bin/sourcecred.js load sourcecred/example-github; ``` The expected behavior of the above command block is that the load command never fails or throws an error. @decentralion and I discussed the scenario where `rimraf` errors. We decided that testing this scenario wasn't necessary, because `rimraf` doesn't error if a directory doesn't exist, and rimraf's maintainer suggests [monkey-patching the fs module] to get rimraf to error in testing scenarios. Thanks @decentralion for reviewing and pair-programming this with me. [monkey-patching the fs module]: https://github.com/isaacs/rimraf/issues/31#issuecomment-29534796
This commit is contained in:
parent
bed476517c
commit
0f038305a2
|
@ -1,7 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Add the `pagerank` command (#1114)
|
||||
- Add the `clear` command (#1111)
|
||||
- Add description tooltips for node and edge types in the weight configuration UI (#1081)
|
||||
- Add the `export-graph` command (#1110)
|
||||
- Enable loading private repositories (#1085)
|
||||
|
@ -11,6 +13,7 @@
|
|||
<!-- Please add new entries to the _top_ of this section. -->
|
||||
|
||||
## [0.2.0]
|
||||
|
||||
- Cache GitHub data, allowing for incremental and resumable loading (#622)
|
||||
- Hyperlink Git commits to GitHub (#887)
|
||||
- Relicense from MIT to MIT + Apache-2 (#812)
|
||||
|
@ -23,6 +26,7 @@
|
|||
- Add `MentionsAuthor` edges to the graph (#808)
|
||||
|
||||
## [0.1.0]
|
||||
|
||||
- Organize weight config by plugin (#773)
|
||||
- Configure edge forward/backward weights separately (#749)
|
||||
- Combine "load graph" and "run pagerank" into one button (#759)
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
"react-router": "3.2.1",
|
||||
"retry": "^0.12.0",
|
||||
"svg-react-loader": "^0.4.6",
|
||||
"rimraf": "^2.6.3",
|
||||
"tmp": "^0.0.33",
|
||||
"whatwg-fetch": "2.0.3"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
// @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;
|
|
@ -0,0 +1,134 @@
|
|||
// @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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,6 +8,7 @@ import {help as loadHelp} from "./load";
|
|||
import {help as analyzeHelp} from "./analyze";
|
||||
import {help as pagerankHelp} from "./pagerank";
|
||||
import {help as exportGraphHelp} from "./exportGraph";
|
||||
import {help as clearHelp} from "./clear";
|
||||
|
||||
const help: Command = async (args, std) => {
|
||||
if (args.length === 0) {
|
||||
|
@ -20,6 +21,7 @@ const help: Command = async (args, std) => {
|
|||
load: loadHelp,
|
||||
analyze: analyzeHelp,
|
||||
pagerank: pagerankHelp,
|
||||
clear: clearHelp,
|
||||
"export-graph": exportGraphHelp,
|
||||
};
|
||||
if (subHelps[command] !== undefined) {
|
||||
|
@ -43,6 +45,7 @@ function usage(print: (string) => void): void {
|
|||
analyze analyze cred for a loaded repository
|
||||
export-graph print a raw SourceCred graph
|
||||
pagerank recompute cred scores
|
||||
clear clear SoucrceCred data
|
||||
help show this help message
|
||||
|
||||
Use 'sourcecred help COMMAND' for help about an individual command.
|
||||
|
|
|
@ -55,6 +55,16 @@ describe("cli/help", () => {
|
|||
});
|
||||
});
|
||||
|
||||
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,
|
||||
|
|
|
@ -10,6 +10,7 @@ import load from "./load";
|
|||
import analyze from "./analyze";
|
||||
import exportGraph from "./exportGraph";
|
||||
import pagerank from "./pagerank";
|
||||
import clear from "./clear";
|
||||
|
||||
const sourcecred: Command = async (args, std) => {
|
||||
if (args.length === 0) {
|
||||
|
@ -27,6 +28,8 @@ const sourcecred: Command = async (args, std) => {
|
|||
return load(args.slice(1), std);
|
||||
case "analyze":
|
||||
return analyze(args.slice(1), std);
|
||||
case "clear":
|
||||
return clear(args.slice(1), std);
|
||||
case "export-graph":
|
||||
return exportGraph(args.slice(1), std);
|
||||
case "pagerank":
|
||||
|
|
|
@ -16,6 +16,7 @@ jest.mock("./load", () => mockCommand("load"));
|
|||
jest.mock("./analyze", () => mockCommand("analyze"));
|
||||
jest.mock("./exportGraph", () => mockCommand("export-graph"));
|
||||
jest.mock("./pagerank", () => mockCommand("pagerank"));
|
||||
jest.mock("./clear", () => mockCommand("clear"));
|
||||
|
||||
describe("cli/sourcecred", () => {
|
||||
it("fails with usage when invoked with no arguments", async () => {
|
||||
|
@ -84,6 +85,14 @@ describe("cli/sourcecred", () => {
|
|||
});
|
||||
});
|
||||
|
||||
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,
|
||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -3692,6 +3692,18 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
|
|||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
|
||||
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
global-modules@1.0.0, global-modules@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
|
||||
|
@ -7483,6 +7495,13 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.
|
|||
dependencies:
|
||||
glob "^7.0.5"
|
||||
|
||||
rimraf@^2.6.3:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
|
||||
|
|
Loading…
Reference in New Issue