Skip to content

Commit

Permalink
Add color.status config support to address twosigma#713
Browse files Browse the repository at this point in the history
  • Loading branch information
clsater committed Dec 3, 2021
1 parent d5e7c73 commit 69be99d
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 18 deletions.
11 changes: 9 additions & 2 deletions node/lib/cmd/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down
40 changes: 40 additions & 0 deletions node/lib/util/color_handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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) {
colorsSafe.enable();
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;
49 changes: 46 additions & 3 deletions node/lib/util/config_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down
52 changes: 41 additions & 11 deletions node/lib/util/print_status_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`;
}
Expand All @@ -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);
Expand Down Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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`;
Expand Down
39 changes: 39 additions & 0 deletions node/test/util/color_handler.js
Original file line number Diff line number Diff line change
@@ -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"));
});
});
});
});
67 changes: 65 additions & 2 deletions node/test/util/config_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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": {
Expand All @@ -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];
Expand All @@ -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);
}
}));
});
});
Expand Down

0 comments on commit 69be99d

Please sign in to comment.