diff --git a/node/lib/cmd/status.js b/node/lib/cmd/status.js index abcd371e..aa3739fe 100644 --- a/node/lib/cmd/status.js +++ b/node/lib/cmd/status.js @@ -100,6 +100,8 @@ exports.executeableSubcommand = co.wrap(function *(args) { const GitUtil = require("../util/git_util"); const PrintStatusUtil = require("../util/print_status_util"); const StatusUtil = require("../util/status_util"); + const ConfigUtil = require("../../lib/util/config_util"); + const ColorHandler = require("../util/color_handler").ColorHandler; const repo = yield GitUtil.getCurrentRepo(); const workdir = repo.workdir(); @@ -115,11 +117,16 @@ exports.executeableSubcommand = co.wrap(function *(args) { const relCwd = path.relative(workdir, cwd); + const colors = new ColorHandler( + yield ConfigUtil.getConfigColorBool(repo, "color.status")); + let text; if (args.shortFormat) { - text = PrintStatusUtil.printRepoStatusShort(repoStatus, relCwd); + text = PrintStatusUtil.printRepoStatusShort(repoStatus, relCwd, + {colors: colors}); } else { - text = PrintStatusUtil.printRepoStatus(repoStatus, relCwd); + text = PrintStatusUtil.printRepoStatus(repoStatus, relCwd, + {colors: colors}); } process.stdout.write(text); diff --git a/node/lib/util/color_handler.js b/node/lib/util/color_handler.js new file mode 100644 index 00000000..0c82326c --- /dev/null +++ b/node/lib/util/color_handler.js @@ -0,0 +1,39 @@ +const colorsSafe = require("colors/safe"); + +/** + * This class is a wrapper around colors/safe that uses a color setting from + * the git meta config to determine if colors should be enabled. + */ +class ColorHandler { + /** + * @param {Bool|"auto"|null} enableColor + * + * NOTE: enableColor should probably be set based on a value in + * ConfigUtil.getConfigColorBool() + */ + constructor(enableColor) { + if(enableColor === "auto" || enableColor === undefined) { + // Enable color if we're outputting to a terminal, otherwise disable + // since we're piping the output. + enableColor = process.stdout.isTTY === true; + } + + this.enableColor = enableColor; + + let self = this; + + // add a passthrough function for each supported color + ["blue", "cyan", "green", "grey", "magenta", "red", "yellow"].forEach( + function(color) { + self[color] = function(string) { + if(self.enableColor) { + return colorsSafe[color](string); + } else { + return string; + } + }; + }); + } +} + +exports.ColorHandler = ColorHandler; diff --git a/node/lib/util/config_util.js b/node/lib/util/config_util.js index 8e1cf9ed..93d015cd 100644 --- a/node/lib/util/config_util.js +++ b/node/lib/util/config_util.js @@ -68,7 +68,7 @@ exports.getConfigString = co.wrap(function *(config, key) { * @param {NodeGit.Repository} repo * @param {NodeGit.Commit} configVar * @return {Bool|null} - * @throws if the configuration variable doesn't exist + * @throws if the configuration value isn't valid. */ exports.configIsTrue = co.wrap(function*(repo, configVar) { assert.instanceOf(repo, NodeGit.Repository); @@ -79,10 +79,53 @@ exports.configIsTrue = co.wrap(function*(repo, configVar) { if (null === configured) { return configured; // RETURN } - return configured === "true" || configured === "yes" || - configured === "on"; + if(configured === "true" || configured === "yes" || + configured === "on" || configured === "1") { + return true; + } else if (configured === "false" || configured === "no" || + configured === "off" || configured === "0") { + return false; + } else { + throw new UserError("fatal: bad boolean config value '" + + configured + "' for '" + configVar + "'"); + } }); +/** + * Returns whether a color boolean is true. If the values is auto, pass + * the value along so it can infer whether colors should be used. + * @async + * @param {NodeGit.Repository} repo + * @param {NodeGit.Commit} configVar + * @return {Bool|"auto"} + */ +exports.getConfigColorBool = co.wrap(function*(repo, configVar) { + // using same logic from git_config_colorbool() in git's color.c + // except, if unset, use default rather than color.ui becuase that's not + // defined right now. + assert.instanceOf(repo, NodeGit.Repository); + assert.isString(configVar); + + const config = yield repo.config(); + const val = yield exports.getConfigString(config, configVar); + + if(val === "never") { + return false; + } else if(val === "always") { + return true; + } else if(val === "auto") { + return "auto"; + } + + const is_true = yield exports.configIsTrue(repo, configVar); + + // a truthy or unset value implies "auto" + if(is_true === null || is_true) { + return "auto"; + } else { + return false; + } +}); /** * Returns the default Signature for a repo. Replaces repo.defaultSignature, diff --git a/node/lib/util/print_status_util.js b/node/lib/util/print_status_util.js index 157583eb..d5f7e861 100644 --- a/node/lib/util/print_status_util.js +++ b/node/lib/util/print_status_util.js @@ -36,14 +36,15 @@ * */ -const assert = require("chai").assert; -const colors = require("colors/safe"); -const path = require("path"); +const assert = require("chai").assert; +const path = require("path"); const GitUtil = require("./git_util"); const SequencerState = require("./sequencer_state"); const RepoStatus = require("./repo_status"); const TextUtil = require("./text_util"); +const ColorHandler = require("./color_handler").ColorHandler; + /** * This value-semantic class describes a line entry to be printed in a status @@ -383,7 +384,14 @@ A ${command} is in progress. * @param {RepoStatus} status * @return {String> */ -exports.printCurrentBranch = function (status) { +exports.printCurrentBranch = function (status, options) { + // fill in default value + if (options === undefined) {options = {};} + if (options.colors === undefined) { + options.colors = new ColorHandler(); + } + let colors = options.colors; + if (null !== status.currentBranchName) { return `On branch ${colors.green(status.currentBranchName)}.\n`; } @@ -399,16 +407,26 @@ On detached head ${colors.red(GitUtil.shortSha(status.headCommit))}.\n`; * the specified `cwd`. Note that a value of "" for `cwd` indicates the root * of the repository. * - * @param {RepoStatus} status - * @param {String} cwd + * @param {RepoStatus} status + * @param {String} cwd + * @param {Object} [options] + * @param {ColorHandler} [options.colors] */ -exports.printRepoStatus = function (status, cwd) { +exports.printRepoStatus = function (status, cwd, options) { assert.instanceOf(status, RepoStatus); assert.isString(cwd); + // fill in default value + if (options === undefined) {options = {};} + if (options.colors === undefined) { + options.colors = new ColorHandler(); + } + + const colors = options.colors; + let result = ""; - result += exports.printCurrentBranch(status); + result += exports.printCurrentBranch(status, options); if (null !== status.sequencerState) { result += exports.printSequencer(status.sequencerState); @@ -464,13 +482,23 @@ Untracked files: * paths relative to the specified `cwd`. Note that a value of "" for * `cwd` indicates the root of the repository. * - * @param {RepoStatus} status - * @param {String} cwd + * @param {RepoStatus} status + * @param {String} cwd + * @param {Object} [options] + * @param {ColorHandler} [options.colors] */ -exports.printRepoStatusShort = function (status, cwd) { +exports.printRepoStatusShort = function (status, cwd, options) { assert.instanceOf(status, RepoStatus); assert.isString(cwd); + // fill in default values for options + if (options === undefined) { options = {}; } + if (options.colors === undefined) { + options.colors = new ColorHandler(); + } + + const colors = options.colors; + let result = ""; const indexChangesByPath = {}; @@ -540,6 +568,8 @@ exports.printSubmoduleStatus = function (relCwd, assert.isObject(subsToPrint); assert.isBoolean(showClosed); + const colors = new ColorHandler("auto"); + let result = ""; if (showClosed) { result = `${colors.grey("All submodules:")}\n`; diff --git a/node/test/util/color_handler.js b/node/test/util/color_handler.js new file mode 100644 index 00000000..9a8f7f64 --- /dev/null +++ b/node/test/util/color_handler.js @@ -0,0 +1,39 @@ +const assert = require("chai").assert; +const ColorHandler = require("../../lib/util/color_handler").ColorHandler; + +const realProcess = process; +describe("ColorHandler", function() { + describe("colors", function() { + describe("enableColor = 'auto'", function() { + afterEach(function() { + // this test futzes around with the process global so + // make sure it is properly reset after each test. + global.process = realProcess; + }); + it("enables color when in a TTY", function() { + global.process = {stdout: {isTTY: true}}; + const colors = new ColorHandler("auto"); + assert.isTrue(colors.enableColor); + }); + + it("disables color when not in a TTY", function() { + global.process = {stdout: {isTTY: false}}; + const colors = new ColorHandler("auto"); + assert.isFalse(colors.enableColor); + assert.equal("test", colors.blue("test")); + }); + + it("adds colors if enableColor", function() { + const colors = new ColorHandler(); + colors.enableColor = true; + assert.equal("\u001b[34mtest\u001b[39m", colors.blue("test")); + }); + + it("passes through string if !enableColor", function() { + const colors = new ColorHandler(); + colors.enableColor = false; + assert.equal("test", colors.blue("test")); + }); + }); + }); +}); diff --git a/node/test/util/config_util.js b/node/test/util/config_util.js index 27fdbc87..9d905289 100644 --- a/node/test/util/config_util.js +++ b/node/test/util/config_util.js @@ -37,6 +37,7 @@ const path = require("path"); const ConfigUtil = require("../../lib/util/config_util"); const TestUtil = require("../../lib/util/test_util"); +const UserError = require("../../lib/util/user_error"); describe("ConfigUtil", function () { describe("getConfigString", function () { @@ -55,6 +56,49 @@ describe("getConfigString", function () { assert.isNull(badResult); })); }); + +describe("getConfigColorBool", function() { + const cases = { + "never": { + value: "never", + expected: false, + }, + "auto": { + value: "auto", + expected: "auto", + }, + "unspecified": { + expected: "auto", + }, + "true": { + value: "true", + expected: "auto", + }, + "always": { + value: "always", + expected: true, + }, + }; + + Object.keys(cases).forEach(caseName => { + const c = cases[caseName]; + it(caseName, co.wrap(function *() { + const repo = yield TestUtil.createSimpleRepository(); + if ("value" in c) { + const configPath = path.join(repo.path(), "config"); + yield fs.appendFile(configPath, `\ +[foo] + bar = ${c.value} +`); + } + const result = yield ConfigUtil.getConfigColorBool(repo, + "foo.bar"); + assert.equal(result, c.expected); + })); + }); +}); + + describe("configIsTrue", function () { const cases = { "missing": { @@ -76,6 +120,11 @@ describe("configIsTrue", function () { value: "on", expected: true, }, + "invalid value": { + value: "asdf", + expected: new UserError( + "fatal: bad boolean config value 'asdf' for 'foo.bar'"), + }, }; Object.keys(cases).forEach(caseName => { const c = cases[caseName]; @@ -88,8 +137,22 @@ describe("configIsTrue", function () { bar = ${c.value} `); } - const result = yield ConfigUtil.configIsTrue(repo, "foo.bar"); - assert.equal(result, c.expected); + let thrownException = null; + let result = null; + try { + result = yield ConfigUtil.configIsTrue(repo, "foo.bar"); + } catch (e) { + thrownException = e; + } + if(c.expected instanceof UserError) { + assert.equal(c.expected.message, thrownException.message); + } else { + // we didn't expect an exception. Rethrow it. + if(thrownException !== null) { + throw thrownException; + } + assert.equal(result, c.expected); + } })); }); });