Skip to content

Commit 26aa918

Browse files
author
Charles Saternos
committed
Add color.status config support to address #713
1 parent d5e7c73 commit 26aa918

File tree

6 files changed

+239
-18
lines changed

6 files changed

+239
-18
lines changed

node/lib/cmd/status.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ exports.executeableSubcommand = co.wrap(function *(args) {
100100
const GitUtil = require("../util/git_util");
101101
const PrintStatusUtil = require("../util/print_status_util");
102102
const StatusUtil = require("../util/status_util");
103+
const ConfigUtil = require("../../lib/util/config_util");
104+
const ColorHandler = require("../util/color_handler").ColorHandler;
103105

104106
const repo = yield GitUtil.getCurrentRepo();
105107
const workdir = repo.workdir();
@@ -115,11 +117,16 @@ exports.executeableSubcommand = co.wrap(function *(args) {
115117

116118
const relCwd = path.relative(workdir, cwd);
117119

120+
const colors = new ColorHandler(
121+
yield ConfigUtil.getConfigColorBool(repo, "color.status"));
122+
118123
let text;
119124
if (args.shortFormat) {
120-
text = PrintStatusUtil.printRepoStatusShort(repoStatus, relCwd);
125+
text = PrintStatusUtil.printRepoStatusShort(repoStatus, relCwd,
126+
{colors: colors});
121127
} else {
122-
text = PrintStatusUtil.printRepoStatus(repoStatus, relCwd);
128+
text = PrintStatusUtil.printRepoStatus(repoStatus, relCwd,
129+
{colors: colors});
123130
}
124131

125132
process.stdout.write(text);

node/lib/util/color_handler.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const colorsSafe = require("colors/safe");
2+
3+
/**
4+
* This class is a wrapper around colors/safe that uses a color setting from
5+
* the git meta config to determine if colors should be enabled.
6+
*/
7+
class ColorHandler {
8+
/**
9+
* @param {Bool|"auto"|null} enableColor
10+
*
11+
* NOTE: enableColor should probably be set based on a value in
12+
* ConfigUtil.getConfigColorBool()
13+
*/
14+
constructor(enableColor) {
15+
if(enableColor === "auto" || enableColor === undefined) {
16+
// Enable color if we're outputting to a terminal, otherwise disable
17+
// since we're piping the output.
18+
enableColor = process.stdout.isTTY === true;
19+
}
20+
21+
this.enableColor = enableColor;
22+
23+
let self = this;
24+
25+
// add a passthrough function for each supported color
26+
["blue", "cyan", "green", "grey", "magenta", "red", "yellow"].forEach(
27+
function(color) {
28+
self[color] = function(string) {
29+
if(self.enableColor) {
30+
return colorsSafe[color](string);
31+
} else {
32+
return string;
33+
}
34+
};
35+
});
36+
}
37+
}
38+
39+
exports.ColorHandler = ColorHandler;

node/lib/util/config_util.js

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ exports.getConfigString = co.wrap(function *(config, key) {
6868
* @param {NodeGit.Repository} repo
6969
* @param {NodeGit.Commit} configVar
7070
* @return {Bool|null}
71-
* @throws if the configuration variable doesn't exist
71+
* @throws if the configuration value isn't valid.
7272
*/
7373
exports.configIsTrue = co.wrap(function*(repo, configVar) {
7474
assert.instanceOf(repo, NodeGit.Repository);
@@ -79,10 +79,53 @@ exports.configIsTrue = co.wrap(function*(repo, configVar) {
7979
if (null === configured) {
8080
return configured; // RETURN
8181
}
82-
return configured === "true" || configured === "yes" ||
83-
configured === "on";
82+
if(configured === "true" || configured === "yes" ||
83+
configured === "on" || configured === "1") {
84+
return true;
85+
} else if (configured === "false" || configured === "no" ||
86+
configured === "off" || configured === "0") {
87+
return false;
88+
} else {
89+
throw new UserError("fatal: bad boolean config value '" +
90+
configured + "' for '" + configVar + "'");
91+
}
8492
});
8593

94+
/**
95+
* Returns whether a color boolean is true. If the values is auto, pass
96+
* the value along so it can infer whether colors should be used.
97+
* @async
98+
* @param {NodeGit.Repository} repo
99+
* @param {NodeGit.Commit} configVar
100+
* @return {Bool|"auto"}
101+
*/
102+
exports.getConfigColorBool = co.wrap(function*(repo, configVar) {
103+
// using same logic from git_config_colorbool() in git's color.c
104+
// except, if unset, use default rather than color.ui becuase that's not
105+
// defined right now.
106+
assert.instanceOf(repo, NodeGit.Repository);
107+
assert.isString(configVar);
108+
109+
const config = yield repo.config();
110+
const val = yield exports.getConfigString(config, configVar);
111+
112+
if(val === "never") {
113+
return false;
114+
} else if(val === "always") {
115+
return true;
116+
} else if(val === "auto") {
117+
return "auto";
118+
}
119+
120+
const is_true = yield exports.configIsTrue(repo, configVar);
121+
122+
// a truthy or unset value implies "auto"
123+
if(is_true === null || is_true) {
124+
return "auto";
125+
} else {
126+
return false;
127+
}
128+
});
86129

87130
/**
88131
* Returns the default Signature for a repo. Replaces repo.defaultSignature,

node/lib/util/print_status_util.js

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,15 @@
3636
*
3737
*/
3838

39-
const assert = require("chai").assert;
40-
const colors = require("colors/safe");
41-
const path = require("path");
39+
const assert = require("chai").assert;
40+
const path = require("path");
4241

4342
const GitUtil = require("./git_util");
4443
const SequencerState = require("./sequencer_state");
4544
const RepoStatus = require("./repo_status");
4645
const TextUtil = require("./text_util");
46+
const ColorHandler = require("./color_handler").ColorHandler;
47+
4748

4849
/**
4950
* This value-semantic class describes a line entry to be printed in a status
@@ -383,7 +384,14 @@ A ${command} is in progress.
383384
* @param {RepoStatus} status
384385
* @return {String>
385386
*/
386-
exports.printCurrentBranch = function (status) {
387+
exports.printCurrentBranch = function (status, options) {
388+
// fill in default value
389+
if (options === undefined) {options = {};}
390+
if (options.colors === undefined) {
391+
options.colors = new ColorHandler();
392+
}
393+
let colors = options.colors;
394+
387395
if (null !== status.currentBranchName) {
388396
return `On branch ${colors.green(status.currentBranchName)}.\n`;
389397
}
@@ -399,16 +407,26 @@ On detached head ${colors.red(GitUtil.shortSha(status.headCommit))}.\n`;
399407
* the specified `cwd`. Note that a value of "" for `cwd` indicates the root
400408
* of the repository.
401409
*
402-
* @param {RepoStatus} status
403-
* @param {String} cwd
410+
* @param {RepoStatus} status
411+
* @param {String} cwd
412+
* @param {Object} [options]
413+
* @param {ColorHandler} [options.colors]
404414
*/
405-
exports.printRepoStatus = function (status, cwd) {
415+
exports.printRepoStatus = function (status, cwd, options) {
406416
assert.instanceOf(status, RepoStatus);
407417
assert.isString(cwd);
408418

419+
// fill in default value
420+
if (options === undefined) {options = {};}
421+
if (options.colors === undefined) {
422+
options.colors = new ColorHandler();
423+
}
424+
425+
const colors = options.colors;
426+
409427
let result = "";
410428

411-
result += exports.printCurrentBranch(status);
429+
result += exports.printCurrentBranch(status, options);
412430

413431
if (null !== status.sequencerState) {
414432
result += exports.printSequencer(status.sequencerState);
@@ -464,13 +482,23 @@ Untracked files:
464482
* paths relative to the specified `cwd`. Note that a value of "" for
465483
* `cwd` indicates the root of the repository.
466484
*
467-
* @param {RepoStatus} status
468-
* @param {String} cwd
485+
* @param {RepoStatus} status
486+
* @param {String} cwd
487+
* @param {Object} [options]
488+
* @param {ColorHandler} [options.colors]
469489
*/
470-
exports.printRepoStatusShort = function (status, cwd) {
490+
exports.printRepoStatusShort = function (status, cwd, options) {
471491
assert.instanceOf(status, RepoStatus);
472492
assert.isString(cwd);
473493

494+
// fill in default values for options
495+
if (options === undefined) { options = {}; }
496+
if (options.colors === undefined) {
497+
options.colors = new ColorHandler();
498+
}
499+
500+
const colors = options.colors;
501+
474502
let result = "";
475503

476504
const indexChangesByPath = {};
@@ -540,6 +568,8 @@ exports.printSubmoduleStatus = function (relCwd,
540568
assert.isObject(subsToPrint);
541569
assert.isBoolean(showClosed);
542570

571+
const colors = new ColorHandler("auto");
572+
543573
let result = "";
544574
if (showClosed) {
545575
result = `${colors.grey("All submodules:")}\n`;

node/test/util/color_handler.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const assert = require("chai").assert;
2+
const ColorHandler = require("../../lib/util/color_handler").ColorHandler;
3+
4+
const realProcess = process;
5+
describe("ColorHandler", function() {
6+
describe("colors", function() {
7+
describe("enableColor = 'auto'", function() {
8+
afterEach(function() {
9+
// this test futzes around with the process global so
10+
// make sure it is properly reset after each test.
11+
global.process = realProcess;
12+
});
13+
it("enables color when in a TTY", function() {
14+
global.process = {stdout: {isTTY: true}};
15+
const colors = new ColorHandler("auto");
16+
assert.isTrue(colors.enableColor);
17+
});
18+
19+
it("disables color when not in a TTY", function() {
20+
global.process = {stdout: {isTTY: false}};
21+
const colors = new ColorHandler("auto");
22+
assert.isFalse(colors.enableColor);
23+
assert.equal("test", colors.blue("test"));
24+
});
25+
26+
it("adds colors if enableColor", function() {
27+
const colors = new ColorHandler();
28+
colors.enableColor = true;
29+
assert.equal("\u001b[34mtest\u001b[39m", colors.blue("test"));
30+
});
31+
32+
it("passes through string if !enableColor", function() {
33+
const colors = new ColorHandler();
34+
colors.enableColor = false;
35+
assert.equal("test", colors.blue("test"));
36+
});
37+
});
38+
});
39+
});

node/test/util/config_util.js

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@
3131
"use strict";
3232

3333
const assert = require("chai").assert;
34+
const expect = require("chai").expect;
3435
const co = require("co");
3536
const fs = require("fs-promise");
3637
const path = require("path");
3738

3839
const ConfigUtil = require("../../lib/util/config_util");
3940
const TestUtil = require("../../lib/util/test_util");
41+
const UserError = require("../../lib/util/user_error");
4042

4143
describe("ConfigUtil", function () {
4244
describe("getConfigString", function () {
@@ -55,6 +57,48 @@ describe("getConfigString", function () {
5557
assert.isNull(badResult);
5658
}));
5759
});
60+
61+
describe("getConfigColorBool", function() {
62+
const cases = {
63+
"never": {
64+
value: "never",
65+
expected: false,
66+
},
67+
"auto": {
68+
value: "auto",
69+
expected: "auto",
70+
},
71+
"unspecified": {
72+
expected: "auto",
73+
},
74+
"true": {
75+
value: "true",
76+
expected: "auto",
77+
},
78+
"always": {
79+
value: "always",
80+
expected: true,
81+
},
82+
};
83+
84+
Object.keys(cases).forEach(caseName => {
85+
const c = cases[caseName];
86+
it(caseName, co.wrap(function *() {
87+
const repo = yield TestUtil.createSimpleRepository();
88+
if ("value" in c) {
89+
const configPath = path.join(repo.path(), "config");
90+
yield fs.appendFile(configPath, `\
91+
[foo]
92+
bar = ${c.value}
93+
`);
94+
}
95+
const result = yield ConfigUtil.getConfigColorBool(repo, "foo.bar");
96+
assert.equal(result, c.expected);
97+
}));
98+
});
99+
});
100+
101+
58102
describe("configIsTrue", function () {
59103
const cases = {
60104
"missing": {
@@ -76,6 +120,11 @@ describe("configIsTrue", function () {
76120
value: "on",
77121
expected: true,
78122
},
123+
"invalid value": {
124+
value: "asdf",
125+
expected: new UserError(
126+
"fatal: bad boolean config value 'asdf' for 'foo.bar'"),
127+
},
79128
};
80129
Object.keys(cases).forEach(caseName => {
81130
const c = cases[caseName];
@@ -88,8 +137,22 @@ describe("configIsTrue", function () {
88137
bar = ${c.value}
89138
`);
90139
}
91-
const result = yield ConfigUtil.configIsTrue(repo, "foo.bar");
92-
assert.equal(result, c.expected);
140+
let thrownException = null;
141+
let result = null;
142+
try {
143+
result = yield ConfigUtil.configIsTrue(repo, "foo.bar");
144+
} catch (e) {
145+
thrownException = e;
146+
}
147+
if(c.expected instanceof UserError) {
148+
assert.equal(c.expected.message, thrownException.message);
149+
} else {
150+
// we didn't expect an exception. Rethrow it.
151+
if(thrownException !== null) {
152+
throw thrownException;
153+
}
154+
assert.equal(result, c.expected);
155+
}
93156
}));
94157
});
95158
});

0 commit comments

Comments
 (0)